dependabot-pre_commit 0.362.0 → 0.364.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4218be0a0ac564f79fe6b570a86323967d5fa38924801f1260e9e314dacf72bd
4
- data.tar.gz: 11184797fefef083921b31eef2b74ca04a27e70ddf0e3f48cc7819554a8c2c56
3
+ metadata.gz: d1bd3f81f06b4e86d4139cf07b731129092edf1f86f4febc799cefb7d67fd03e
4
+ data.tar.gz: 1fa2859dd9dafc4b09ab7718de4b59d2cff583c5991c5439644b40fb2c4a88c8
5
5
  SHA512:
6
- metadata.gz: 6963ef88985b49c16264d24f11b0da7857673a8c40c166bbb0b0d4fa8a03163a0b3422b1b13b5a0ba869413ca688b95f6654410fc460ff82959bde6d52aa0ab7
7
- data.tar.gz: adcecfc80611486b0446a56cc6a95d011b1213e4266aff09b9bd0623fff26c1e87f513726449c8f4e7a34a3f1919cf1ba98a2e9f604696dc0f4cb5335b176e8b
6
+ metadata.gz: e3c7c74f6e6565ca5ecf32734d4a9d7af200e8acaabec1856abb839815b5d26360375c915104a7b4c484abf0333e68685029ad5b793ab43da0179893f5088507
7
+ data.tar.gz: e1f3f69285a2cf37e1072ae5216693f51d764c48ef2de33dda2d669943900e41f085e4ec6a8529f761a11fedb58a2c82867a2ef9ab67fdb2b80c47942931203e
@@ -0,0 +1,175 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/dependency"
6
+ require "dependabot/update_checkers"
7
+ require "dependabot/pub/version"
8
+ require "dependabot/pre_commit/additional_dependency_checkers"
9
+ require "dependabot/pre_commit/additional_dependency_checkers/base"
10
+
11
+ module Dependabot
12
+ module PreCommit
13
+ module AdditionalDependencyCheckers
14
+ class Dart < Base
15
+ extend T::Sig
16
+
17
+ sig { override.returns(T.nilable(String)) }
18
+ def latest_version
19
+ return nil unless package_name
20
+
21
+ @latest_version ||= T.let(
22
+ fetch_latest_version_via_pub_checker,
23
+ T.nilable(String)
24
+ )
25
+ end
26
+
27
+ sig { override.params(latest_version: String).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
28
+ def updated_requirements(latest_version)
29
+ requirements.map do |original_req|
30
+ original_source = original_req[:source]
31
+ next original_req unless original_source.is_a?(Hash)
32
+ next original_req unless original_source[:type] == "additional_dependency"
33
+
34
+ original_requirement = original_req[:requirement]
35
+ new_requirement = build_updated_requirement(original_requirement, latest_version)
36
+
37
+ new_original_string = build_original_string(
38
+ package_name: original_source[:original_name] || original_source[:package_name],
39
+ requirement: new_requirement
40
+ )
41
+
42
+ new_source = original_source.merge(original_string: new_original_string)
43
+
44
+ original_req.merge(
45
+ requirement: new_requirement,
46
+ source: new_source
47
+ )
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ sig { returns(T.nilable(String)) }
54
+ def fetch_latest_version_via_pub_checker
55
+ pub_checker = pub_update_checker
56
+ return nil unless pub_checker
57
+
58
+ latest = pub_checker.latest_version
59
+ Dependabot.logger.info("Pub UpdateChecker found latest version: #{latest || 'none'}")
60
+
61
+ latest&.to_s
62
+ rescue Dependabot::DependabotError, Excon::Error => e
63
+ Dependabot.logger.debug("Error checking Dart package #{package_name}: #{e.message}")
64
+ nil
65
+ end
66
+
67
+ sig { returns(T.nilable(Dependabot::UpdateCheckers::Base)) }
68
+ def pub_update_checker
69
+ @pub_update_checker ||= T.let(
70
+ build_pub_update_checker,
71
+ T.nilable(Dependabot::UpdateCheckers::Base)
72
+ )
73
+ end
74
+
75
+ sig { returns(T.nilable(Dependabot::UpdateCheckers::Base)) }
76
+ def build_pub_update_checker
77
+ pub_dependency = build_pub_dependency
78
+ return nil unless pub_dependency
79
+
80
+ Dependabot.logger.info("Delegating to pub UpdateChecker for package: #{pub_dependency.name}")
81
+
82
+ Dependabot::UpdateCheckers.for_package_manager("pub").new(
83
+ dependency: pub_dependency,
84
+ dependency_files: build_pub_dependency_files,
85
+ credentials: credentials,
86
+ ignored_versions: [],
87
+ security_advisories: [],
88
+ raise_on_ignored: false
89
+ )
90
+ end
91
+
92
+ sig { returns(T.nilable(Dependabot::Dependency)) }
93
+ def build_pub_dependency
94
+ return nil unless package_name
95
+
96
+ version = current_version || extract_version_from_requirement
97
+
98
+ Dependabot::Dependency.new(
99
+ name: T.must(package_name),
100
+ version: version,
101
+ requirements: [{
102
+ requirement: version ? "^#{version}" : nil,
103
+ groups: ["dependencies"],
104
+ file: "pubspec.yaml",
105
+ source: nil
106
+ }],
107
+ package_manager: "pub"
108
+ )
109
+ end
110
+
111
+ sig { returns(T.nilable(String)) }
112
+ def extract_version_from_requirement
113
+ req_string = requirements.first&.dig(:requirement)
114
+ return nil unless req_string
115
+
116
+ version_part = req_string.to_s.sub(/\A(?:[~^]|[><=]+)\s*/, "")
117
+ return version_part if Dependabot::Pub::Version.correct?(version_part)
118
+
119
+ nil
120
+ end
121
+
122
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
123
+ def build_pub_dependency_files
124
+ version = current_version || extract_version_from_requirement
125
+ requirement = version ? "^#{version}" : "any"
126
+
127
+ pubspec_content = <<~YAML
128
+ name: dependabot_pre_commit_check
129
+ version: 0.0.1
130
+ environment:
131
+ sdk: ">=3.0.0 <4.0.0"
132
+ dependencies:
133
+ #{T.must(package_name)}: #{requirement}
134
+ YAML
135
+
136
+ [
137
+ Dependabot::DependencyFile.new(
138
+ name: "pubspec.yaml",
139
+ content: pubspec_content
140
+ )
141
+ ]
142
+ end
143
+
144
+ sig do
145
+ params(
146
+ package_name: T.nilable(String),
147
+ requirement: T.nilable(String)
148
+ ).returns(String)
149
+ end
150
+ def build_original_string(package_name:, requirement:)
151
+ base = package_name.to_s
152
+ base = "#{base}:#{requirement}" if requirement
153
+ base
154
+ end
155
+
156
+ sig { params(original_requirement: T.nilable(String), new_version: String).returns(String) }
157
+ def build_updated_requirement(original_requirement, new_version)
158
+ return new_version unless original_requirement
159
+
160
+ operator_match = original_requirement.match(/\A(?<op>[~^]|[><=]+)\s*/)
161
+ if operator_match
162
+ "#{operator_match[:op]}#{new_version}"
163
+ else
164
+ new_version
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ Dependabot::PreCommit::AdditionalDependencyCheckers.register(
173
+ "dart",
174
+ Dependabot::PreCommit::AdditionalDependencyCheckers::Dart
175
+ )
@@ -0,0 +1,17 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module PreCommit
6
+ module CommentVersionHelper
7
+ # Matches a version string in a comment, with optional "v" prefix.
8
+ # Examples: "v1", "v2.3.2", "7.3.0", "1.43.5"
9
+ COMMENT_VERSION_PATTERN = T.let(/v?\d+(?:\.\d+)*/, Regexp)
10
+
11
+ # Matches a version string preceded by a "frozen:" label or "#" prefix.
12
+ # Captures the version string (with optional "v" prefix) in group 1.
13
+ # Examples: "# frozen: v2.3.2" → "v2.3.2", "# v4.4.0" → "v4.4.0"
14
+ FROZEN_COMMENT_REF_PATTERN = T.let(/(?:frozen:\s*|#\s*)(#{COMMENT_VERSION_PATTERN})/, Regexp)
15
+ end
16
+ end
17
+ end
@@ -25,13 +25,6 @@ module Dependabot
25
25
 
26
26
  sig { override.returns(T::Array[DependencyFile]) }
27
27
  def fetch_files
28
- unless allow_beta_ecosystems?
29
- raise Dependabot::DependencyFileNotFound.new(
30
- nil,
31
- "PreCommit support is currently in beta. Set ALLOW_BETA_ECOSYSTEMS=true to enable it."
32
- )
33
- end
34
-
35
28
  fetched_files = []
36
29
  fetched_files << pre_commit_config
37
30
 
@@ -0,0 +1,181 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+ require "base64"
6
+ require "sorbet-runtime"
7
+ require "dependabot/clients/github_with_retries"
8
+ require "dependabot/shared_helpers"
9
+ require "dependabot/source"
10
+
11
+ require "dependabot/pre_commit/file_parser"
12
+
13
+ module Dependabot
14
+ module PreCommit
15
+ class FileParser < Dependabot::FileParsers::Base
16
+ # Fetches hook language information from the source repository's
17
+ # .pre-commit-hooks.yaml file. This is needed because the language field
18
+ # is typically defined in the hook repo, not in the consumer's
19
+ # .pre-commit-config.yaml file.
20
+ class HookLanguageFetcher
21
+ extend T::Sig
22
+
23
+ HOOKS_FILE = ".pre-commit-hooks.yaml"
24
+
25
+ sig do
26
+ params(
27
+ credentials: T::Array[Dependabot::Credential]
28
+ ).void
29
+ end
30
+ def initialize(credentials:)
31
+ @credentials = credentials
32
+ @hooks_cache = T.let({}, T::Hash[String, T.nilable(T::Array[T::Hash[String, T.untyped]])])
33
+ end
34
+
35
+ # Fetches the language for a specific hook from the hook source repository.
36
+ #
37
+ # @param repo_url [String] The URL of the hook repository (e.g., "https://github.com/psf/black")
38
+ # @param revision [String] The revision (tag, SHA, branch) to fetch from
39
+ # @param hook_id [String] The hook ID to look up (e.g., "black")
40
+ # @return [String, nil] The language for the hook, or nil if not found
41
+ sig do
42
+ params(
43
+ repo_url: String,
44
+ revision: String,
45
+ hook_id: String
46
+ ).returns(T.nilable(String))
47
+ end
48
+ def fetch_language(repo_url:, revision:, hook_id:)
49
+ hooks = fetch_hooks_from_repo(repo_url, revision)
50
+ return nil unless hooks
51
+
52
+ hook = hooks.find { |h| h["id"] == hook_id }
53
+ return nil unless hook
54
+
55
+ T.cast(hook["language"], T.nilable(String))
56
+ end
57
+
58
+ private
59
+
60
+ sig { returns(T::Array[Dependabot::Credential]) }
61
+ attr_reader :credentials
62
+
63
+ sig do
64
+ params(
65
+ repo_url: String,
66
+ revision: String
67
+ ).returns(T.nilable(T::Array[T::Hash[String, T.untyped]]))
68
+ end
69
+ def fetch_hooks_from_repo(repo_url, revision)
70
+ cache_key = "#{repo_url}@#{revision}"
71
+ return @hooks_cache[cache_key] if @hooks_cache.key?(cache_key)
72
+
73
+ hooks = fetch_hooks_internal(repo_url, revision)
74
+ @hooks_cache[cache_key] = hooks
75
+ hooks
76
+ end
77
+
78
+ sig do
79
+ params(
80
+ repo_url: String,
81
+ revision: String
82
+ ).returns(T.nilable(T::Array[T::Hash[String, T.untyped]]))
83
+ end
84
+ def fetch_hooks_internal(repo_url, revision)
85
+ source = Source.from_url(repo_url)
86
+ return fetch_via_git_clone(repo_url, revision) unless source
87
+ return fetch_via_git_clone(repo_url, revision) unless source.provider == "github"
88
+
89
+ fetch_from_github(source, revision)
90
+ rescue StandardError => e
91
+ Dependabot.logger.debug("Failed to fetch hooks from #{repo_url}@#{revision}: #{e.message}")
92
+ nil
93
+ end
94
+
95
+ sig do
96
+ params(
97
+ source: Dependabot::Source,
98
+ revision: String
99
+ ).returns(T.nilable(T::Array[T::Hash[String, T.untyped]]))
100
+ end
101
+ def fetch_from_github(source, revision)
102
+ response = github_client.send(
103
+ :contents,
104
+ source.repo,
105
+ path: HOOKS_FILE,
106
+ ref: revision
107
+ )
108
+ return nil unless response
109
+
110
+ content = Base64.decode64(response.content)
111
+ parse_hooks_yaml(content)
112
+ rescue Octokit::NotFound
113
+ Dependabot.logger.debug("#{HOOKS_FILE} not found in #{source.repo}@#{revision}")
114
+ nil
115
+ rescue StandardError => e
116
+ Dependabot.logger.debug("Error fetching from GitHub: #{e.message}")
117
+ fetch_via_git_clone("https://github.com/#{source.repo}", revision)
118
+ end
119
+
120
+ sig do
121
+ params(
122
+ repo_url: String,
123
+ revision: String
124
+ ).returns(T.nilable(T::Array[T::Hash[String, T.untyped]]))
125
+ end
126
+ def fetch_via_git_clone(repo_url, revision)
127
+ source = Source.from_url(repo_url)
128
+ return nil unless source
129
+
130
+ SharedHelpers.in_a_temporary_directory(File.dirname(source.repo)) do |temp_dir|
131
+ repo_contents_path = File.join(temp_dir, File.basename(source.repo))
132
+
133
+ SharedHelpers.run_shell_command(
134
+ "git clone --no-checkout --depth 1 #{repo_url} #{repo_contents_path}",
135
+ fingerprint: "git clone --no-checkout --depth 1 <url> <path>"
136
+ )
137
+
138
+ Dir.chdir(repo_contents_path) do
139
+ # Fetch the specific revision and checkout the hooks file
140
+ SharedHelpers.run_shell_command(
141
+ "git fetch --depth 1 origin #{revision}",
142
+ fingerprint: "git fetch --depth 1 origin <revision>"
143
+ )
144
+ SharedHelpers.run_shell_command(
145
+ "git checkout FETCH_HEAD -- #{HOOKS_FILE}",
146
+ fingerprint: "git checkout FETCH_HEAD -- <file>"
147
+ )
148
+
149
+ return nil unless File.exist?(HOOKS_FILE)
150
+
151
+ content = File.read(HOOKS_FILE)
152
+ parse_hooks_yaml(content)
153
+ end
154
+ end
155
+ rescue StandardError => e
156
+ Dependabot.logger.debug("Failed to clone and fetch hooks: #{e.message}")
157
+ nil
158
+ end
159
+
160
+ sig { params(content: String).returns(T.nilable(T::Array[T::Hash[String, T.untyped]])) }
161
+ def parse_hooks_yaml(content)
162
+ yaml = YAML.safe_load(content, aliases: true)
163
+ return nil unless yaml.is_a?(Array)
164
+
165
+ yaml.grep(Hash)
166
+ rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias => e
167
+ Dependabot.logger.debug("Failed to parse hooks YAML: #{e.message}")
168
+ nil
169
+ end
170
+
171
+ sig { returns(Dependabot::Clients::GithubWithRetries) }
172
+ def github_client
173
+ @github_client ||= T.let(
174
+ Dependabot::Clients::GithubWithRetries.for_github_dot_com(credentials: credentials),
175
+ T.nilable(Dependabot::Clients::GithubWithRetries)
176
+ )
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -12,6 +12,7 @@ require "dependabot/pre_commit/version"
12
12
  require "dependabot/pre_commit/requirement"
13
13
  require "dependabot/cargo/requirement"
14
14
  require "dependabot/npm_and_yarn/requirement"
15
+ require "dependabot/pub/requirement"
15
16
  require "dependabot/python/requirement_parser"
16
17
  require "dependabot/bundler/requirement"
17
18
  require "dependabot/go_modules/requirement_parser"
@@ -22,6 +23,7 @@ module Dependabot
22
23
  extend T::Sig
23
24
 
24
25
  require "dependabot/file_parsers/base/dependency_set"
26
+ require_relative "file_parser/hook_language_fetcher"
25
27
 
26
28
  CONFIG_FILE_PATTERN = /\.pre-commit(-config)?\.ya?ml$/i
27
29
  ECOSYSTEM = "pre_commit"
@@ -32,7 +34,8 @@ module Dependabot
32
34
  "node" => ->(dep_string) { Dependabot::NpmAndYarn::Requirement.parse_dep_string(dep_string) },
33
35
  "rust" => ->(dep_string) { Dependabot::Cargo::Requirement.parse_dep_string(dep_string) },
34
36
  "golang" => ->(dep_string) { Dependabot::GoModules::RequirementParser.parse(dep_string) },
35
- "ruby" => ->(dep_string) { Dependabot::Bundler::Requirement.parse_dep_string(dep_string) }
37
+ "ruby" => ->(dep_string) { Dependabot::Bundler::Requirement.parse_dep_string(dep_string) },
38
+ "dart" => ->(dep_string) { Dependabot::Pub::Requirement.parse_dep_string(dep_string) }
36
39
  }.freeze,
37
40
  T::Hash[String, T.proc.params(dep_string: String).returns(T.nilable(T::Hash[Symbol, T.untyped]))]
38
41
  )
@@ -102,6 +105,8 @@ module Dependabot
102
105
  return nil if repo_url.nil? || rev.nil?
103
106
  return nil if %w(local meta).include?(repo_url)
104
107
 
108
+ comment = rev_line_comment(file, repo_url)
109
+
105
110
  Dependency.new(
106
111
  name: repo_url,
107
112
  version: rev,
@@ -114,7 +119,8 @@ module Dependabot
114
119
  url: repo_url,
115
120
  ref: rev,
116
121
  branch: nil
117
- }
122
+ },
123
+ metadata: { comment: comment }
118
124
  }],
119
125
  package_manager: ECOSYSTEM
120
126
  )
@@ -129,6 +135,7 @@ module Dependabot
129
135
  def parse_additional_dependencies(repo, file)
130
136
  dependencies = []
131
137
  repo_url = repo["repo"]
138
+ revision = repo["rev"]
132
139
 
133
140
  return dependencies if repo_url.nil? || %w(local meta).include?(repo_url)
134
141
 
@@ -136,7 +143,7 @@ module Dependabot
136
143
  hooks.each do |hook|
137
144
  next unless hook.is_a?(Hash)
138
145
 
139
- hook_deps = parse_hook_additional_dependencies(hook, repo_url, file)
146
+ hook_deps = parse_hook_additional_dependencies(hook, repo_url, revision, file)
140
147
  dependencies.concat(hook_deps)
141
148
  end
142
149
 
@@ -147,18 +154,24 @@ module Dependabot
147
154
  params(
148
155
  hook: T::Hash[String, T.untyped],
149
156
  repo_url: String,
157
+ revision: T.nilable(String),
150
158
  file: Dependabot::DependencyFile
151
159
  ).returns(T::Array[Dependabot::Dependency])
152
160
  end
153
- def parse_hook_additional_dependencies(hook, repo_url, file)
161
+ def parse_hook_additional_dependencies(hook, repo_url, revision, file)
154
162
  dependencies = []
163
+ hook_id = hook["id"]
155
164
 
156
- return dependencies unless hook["id"]
165
+ return dependencies unless hook_id
157
166
 
158
167
  additional_deps = hook.fetch("additional_dependencies", [])
159
168
  return dependencies if additional_deps.empty?
160
169
 
161
- parser = LANGUAGE_PARSERS[hook["language"]]
170
+ # Get language from local config first, then try fetching from hook source repo
171
+ language = resolve_hook_language(hook, repo_url, revision, hook_id)
172
+ return dependencies unless language
173
+
174
+ parser = LANGUAGE_PARSERS[language]
162
175
  return dependencies unless parser
163
176
 
164
177
  additional_deps.each do |dep_string|
@@ -176,10 +189,10 @@ module Dependabot
176
189
  file: file.name,
177
190
  source: {
178
191
  type: "additional_dependency",
179
- language: hook["language"],
192
+ language: language,
180
193
  package_name: parsed[:normalised_name],
181
194
  original_name: parsed[:name],
182
- hook_id: hook["id"],
195
+ hook_id: hook_id,
183
196
  hook_repo: repo_url,
184
197
  extras: parsed[:extras],
185
198
  original_string: dep_string
@@ -192,6 +205,59 @@ module Dependabot
192
205
  dependencies
193
206
  end
194
207
 
208
+ sig do
209
+ params(
210
+ hook: T::Hash[String, T.untyped],
211
+ repo_url: String,
212
+ revision: T.nilable(String),
213
+ hook_id: String
214
+ ).returns(T.nilable(String))
215
+ end
216
+ def resolve_hook_language(hook, repo_url, revision, hook_id)
217
+ # Use local language if explicitly specified
218
+ local_language = hook["language"]
219
+ return local_language if local_language
220
+
221
+ # Otherwise fetch from the hook source repository
222
+ return nil unless revision
223
+
224
+ hook_language_fetcher.fetch_language(
225
+ repo_url: repo_url,
226
+ revision: revision,
227
+ hook_id: hook_id
228
+ )
229
+ end
230
+
231
+ sig { returns(HookLanguageFetcher) }
232
+ def hook_language_fetcher
233
+ @hook_language_fetcher ||= T.let(
234
+ HookLanguageFetcher.new(credentials: credentials),
235
+ T.nilable(HookLanguageFetcher)
236
+ )
237
+ end
238
+
239
+ sig do
240
+ params(
241
+ file: Dependabot::DependencyFile,
242
+ repo_url: String
243
+ ).returns(T.nilable(String))
244
+ end
245
+ def rev_line_comment(file, repo_url)
246
+ current_repo = T.let(nil, T.nilable(String))
247
+
248
+ T.must(file.content).each_line do |line|
249
+ repo_match = line.match(/^\s*-\s*repo:\s*(\S+)/)
250
+ current_repo = repo_match[1] if repo_match
251
+
252
+ next unless current_repo == repo_url
253
+
254
+ rev_match = line.match(/^\s*rev:\s*\S+\s*(#.*)$/)
255
+ return T.must(rev_match[1]).rstrip if rev_match
256
+ end
257
+
258
+ nil
259
+ end
260
+
195
261
  sig { returns(T::Array[Dependabot::DependencyFile]) }
196
262
  def pre_commit_config_files
197
263
  dependency_files.select { |f| f.name.match?(CONFIG_FILE_PATTERN) }
@@ -113,7 +113,18 @@ module Dependabot
113
113
  old_ref = T.cast(old_source.fetch(:ref), String)
114
114
  new_ref = T.cast(new_source.fetch(:ref), String)
115
115
 
116
- replace_ref_in_content(content, repo_url, old_ref, new_ref)
116
+ new_metadata = T.cast(new_req.fetch(:metadata, {}), T::Hash[Symbol, T.untyped])
117
+ old_version = T.cast(new_metadata[:comment_version], T.nilable(String))
118
+ new_version = T.cast(new_metadata[:new_comment_version], T.nilable(String))
119
+
120
+ replace_ref_in_content(
121
+ content,
122
+ repo_url,
123
+ old_ref,
124
+ new_ref,
125
+ old_version: old_version,
126
+ new_version: new_version
127
+ )
117
128
  end
118
129
 
119
130
  sig do
@@ -134,9 +145,16 @@ module Dependabot
134
145
  end
135
146
 
136
147
  sig do
137
- params(content: String, repo_url: String, old_ref: String, new_ref: String).returns(String)
148
+ params(
149
+ content: String,
150
+ repo_url: String,
151
+ old_ref: String,
152
+ new_ref: String,
153
+ old_version: T.nilable(String),
154
+ new_version: T.nilable(String)
155
+ ).returns(String)
138
156
  end
139
- def replace_ref_in_content(content, repo_url, old_ref, new_ref)
157
+ def replace_ref_in_content(content, repo_url, old_ref, new_ref, old_version: nil, new_version: nil)
140
158
  current_repo = T.let(nil, T.nilable(String))
141
159
 
142
160
  updated_lines = content.lines.map do |line|
@@ -145,7 +163,9 @@ module Dependabot
145
163
 
146
164
  if current_repo == repo_url &&
147
165
  line.match?(/^\s*rev:\s+#{Regexp.escape(old_ref)}(\s*(?:#.*)?)?$/)
148
- line.gsub(old_ref, new_ref)
166
+ updated_line = line.sub(old_ref, new_ref)
167
+ updated_line = update_version_comment(updated_line, old_version, new_version)
168
+ updated_line
149
169
  else
150
170
  line
151
171
  end
@@ -154,6 +174,28 @@ module Dependabot
154
174
  updated_lines.join
155
175
  end
156
176
 
177
+ sig do
178
+ params(
179
+ line: String,
180
+ old_version: T.nilable(String),
181
+ new_version: T.nilable(String)
182
+ ).returns(String)
183
+ end
184
+ def update_version_comment(line, old_version, new_version)
185
+ return line unless old_version && new_version
186
+
187
+ pattern = /
188
+ ( # 1: comment prefix
189
+ \#\s* # '#' and optional whitespace
190
+ (?:frozen:\s*)? # optional 'frozen:' label
191
+ )
192
+ #{Regexp.escape(old_version)} # the old version
193
+ (.*)$ # 2: trailing content (whitespace or other characters) up to end of line
194
+ /x
195
+
196
+ line.sub(pattern, "\\1#{new_version}\\2")
197
+ end
198
+
157
199
  sig do
158
200
  params(content: String, old_string: String, new_string: String).returns(String)
159
201
  end
@@ -1,10 +1,7 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- # NOTE: This file was scaffolded automatically but is OPTIONAL.
5
- # If you don't need custom metadata finding logic (changelogs, release notes, etc.),
6
- # you can safely delete this file and remove the require from lib/dependabot/pre_commit.rb
7
-
4
+ require "sorbet-runtime"
8
5
  require "dependabot/metadata_finders"
9
6
  require "dependabot/metadata_finders/base"
10
7
 
@@ -17,9 +14,15 @@ module Dependabot
17
14
 
18
15
  sig { override.returns(T.nilable(Dependabot::Source)) }
19
16
  def look_up_source
20
- # TODO: Implement custom source lookup logic if needed
21
- # Otherwise, delete this file and the require in the main registration file
22
- nil
17
+ info = dependency.requirements.filter_map { |r| r[:source] }.first
18
+
19
+ url =
20
+ if info.nil?
21
+ dependency.name
22
+ else
23
+ info[:url] || info.fetch("url")
24
+ end
25
+ Source.from_url(url)
23
26
  end
24
27
  end
25
28
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
  require "dependabot/errors"
6
+ require "dependabot/pre_commit/comment_version_helper"
6
7
  require "dependabot/pre_commit/helpers"
7
8
  require "dependabot/pre_commit/requirement"
8
9
  require "dependabot/pre_commit/update_checker"
@@ -71,11 +72,14 @@ module Dependabot
71
72
  def commit_sha_release
72
73
  return unless git_commit_checker.pinned_ref_looks_like_commit_sha?
73
74
 
74
- # Prioritize tagged releases over latest commits
75
- # If latest_version_tag exists, use it (even if current SHA doesn't have a tag)
76
- return latest_version_tag&.fetch(:version) if latest_version_tag
75
+ if latest_version_tag
76
+ if git_commit_checker.local_tag_for_pinned_sha || version_comment?
77
+ return T.must(latest_version_tag).fetch(:version)
78
+ end
79
+
80
+ return latest_commit_for_pinned_ref
81
+ end
77
82
 
78
- # Only fall back to latest commit if no tags exist
79
83
  latest_commit_for_pinned_ref
80
84
  end
81
85
 
@@ -83,7 +87,9 @@ module Dependabot
83
87
  def latest_version_tag
84
88
  @latest_version_tag ||= T.let(
85
89
  begin
86
- return git_commit_checker.local_tag_for_latest_version if dependency.version.nil?
90
+ if dependency.version.nil? || !Dependabot::PreCommit::Version.correct?(dependency.version)
91
+ return constrained_latest_version_tag || git_commit_checker.local_tag_for_latest_version
92
+ end
87
93
 
88
94
  ref = git_commit_checker.local_ref_for_latest_version_matching_existing_precision
89
95
  return ref if ref && current_version && ref.fetch(:version) > current_version
@@ -96,6 +102,74 @@ module Dependabot
96
102
 
97
103
  private
98
104
 
105
+ sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
106
+ def constrained_latest_version_tag
107
+ frozen_ref = frozen_comment_ref
108
+ return nil unless frozen_ref
109
+
110
+ prefix = tag_prefix(frozen_ref)
111
+ return nil if prefix.empty?
112
+
113
+ version_suffix = frozen_ref.sub(/^#{Regexp.escape(prefix)}/, "")
114
+ return nil if version_suffix.empty? || version_suffix !~ /^\d/
115
+
116
+ matching = tags_matching_frozen_constraint(prefix, version_suffix.split("."))
117
+ git_commit_checker.max_local_tag(matching)
118
+ end
119
+
120
+ sig do
121
+ params(
122
+ prefix: String,
123
+ frozen_segments: T::Array[String]
124
+ ).returns(T::Array[Dependabot::GitRef])
125
+ end
126
+ def tags_matching_frozen_constraint(prefix, frozen_segments)
127
+ tags = tags_with_prefix(prefix)
128
+
129
+ max_segments = tags.map { |t| tag_version_segments(t.name, prefix).length }.max || 0
130
+ return tags unless frozen_segments.length < max_segments
131
+
132
+ tags.select { |tag| tag_starts_with_segments?(tag.name, prefix, frozen_segments) }
133
+ end
134
+
135
+ sig { params(prefix: String).returns(T::Array[Dependabot::GitRef]) }
136
+ def tags_with_prefix(prefix)
137
+ git_commit_checker.allowed_version_tags.select { |tag| tag.name.start_with?(prefix) }
138
+ end
139
+
140
+ sig { params(tag_name: String, prefix: String).returns(T::Array[String]) }
141
+ def tag_version_segments(tag_name, prefix)
142
+ tag_name.sub(/^#{Regexp.escape(prefix)}/, "").split(".")
143
+ end
144
+
145
+ sig { params(tag_name: String, prefix: String, frozen_segments: T::Array[String]).returns(T::Boolean) }
146
+ def tag_starts_with_segments?(tag_name, prefix, frozen_segments)
147
+ segments = tag_version_segments(tag_name, prefix)
148
+ frozen_segments.each_with_index.all? { |seg, i| segments[i] == seg }
149
+ end
150
+
151
+ sig { returns(T.nilable(String)) }
152
+ def frozen_comment_ref
153
+ comment = dependency.requirements.first&.dig(:metadata, :comment)
154
+ return nil unless comment
155
+
156
+ match = comment.match(CommentVersionHelper::FROZEN_COMMENT_REF_PATTERN)
157
+ match&.[](1)
158
+ end
159
+
160
+ sig { params(ref: String).returns(String) }
161
+ def tag_prefix(ref)
162
+ ref.sub(/\d+(?:\.\d+)*$/, "")
163
+ end
164
+
165
+ sig { returns(T::Boolean) }
166
+ def version_comment?
167
+ comment = dependency.requirements.first&.dig(:metadata, :comment)
168
+ return false unless comment
169
+
170
+ comment.match?(CommentVersionHelper::COMMENT_VERSION_PATTERN)
171
+ end
172
+
99
173
  sig { returns(T.nilable(String)) }
100
174
  def current_commit
101
175
  git_commit_checker.head_commit_for_current_branch
@@ -4,9 +4,11 @@
4
4
  require "sorbet-runtime"
5
5
 
6
6
  require "dependabot/errors"
7
+ require "dependabot/pre_commit/comment_version_helper"
7
8
  require "dependabot/pre_commit/requirement"
8
9
  require "dependabot/pre_commit/version"
9
10
  require "dependabot/pre_commit/additional_dependency_checkers"
11
+ require "dependabot/pre_commit/additional_dependency_checkers/dart"
10
12
  require "dependabot/pre_commit/additional_dependency_checkers/node"
11
13
  require "dependabot/pre_commit/additional_dependency_checkers/python"
12
14
  require "dependabot/pre_commit/additional_dependency_checkers/ruby"
@@ -54,7 +56,7 @@ module Dependabot
54
56
 
55
57
  current = T.cast(source&.[](:ref), T.nilable(String))
56
58
 
57
- # Maintain a short git hash only if it matches the latest
59
+ # Maintain short git hash when the updated SHA starts with the current SHA
58
60
  if T.cast(req[:type], T.nilable(String)) == "git" &&
59
61
  git_commit_checker.ref_looks_like_commit_sha?(updated) &&
60
62
  current && git_commit_checker.ref_looks_like_commit_sha?(current) &&
@@ -63,7 +65,8 @@ module Dependabot
63
65
  end
64
66
 
65
67
  new_source = T.must(source).merge(ref: updated)
66
- req.merge(source: new_source)
68
+ new_metadata = updated_comment_version_metadata(req, updated)
69
+ req.merge(source: new_source, metadata: new_metadata)
67
70
  end
68
71
  end
69
72
 
@@ -89,6 +92,9 @@ module Dependabot
89
92
  def current_version
90
93
  return super if dependency.numeric_version
91
94
 
95
+ frozen_ver = version_from_comment
96
+ return frozen_ver if frozen_ver
97
+
92
98
  # For git dependencies, try to parse the version from the ref
93
99
  source_details = dependency.source_details(allowed_types: ["git"])
94
100
  return nil unless source_details
@@ -102,6 +108,17 @@ module Dependabot
102
108
  version_class.new(version_string)
103
109
  end
104
110
 
111
+ sig { returns(T::Boolean) }
112
+ def sha1_version_up_to_date?
113
+ frozen_ver = version_from_comment
114
+ return super unless frozen_ver
115
+
116
+ resolved_sha = latest_commit_sha
117
+ return true if resolved_sha && resolved_sha == dependency.version
118
+
119
+ false
120
+ end
121
+
105
122
  sig { override.returns(T::Boolean) }
106
123
  def latest_version_resolvable_with_full_unlock?
107
124
  false
@@ -166,10 +183,11 @@ module Dependabot
166
183
  new_tag = T.must(latest_version_finder).latest_version_tag
167
184
 
168
185
  if new_tag
169
- return T.cast(new_tag.fetch(:commit_sha), String) if git_commit_checker.local_tag_for_pinned_sha
186
+ if version_from_comment || git_commit_checker.local_tag_for_pinned_sha
187
+ return T.cast(new_tag.fetch(:commit_sha), String)
188
+ end
170
189
 
171
190
  return latest_commit_for_pinned_ref
172
-
173
191
  end
174
192
 
175
193
  # If there's no tag but we have a latest_version (commit SHA), use it
@@ -184,6 +202,67 @@ module Dependabot
184
202
  @git_commit_checker ||= T.let(git_helper.git_commit_checker, T.nilable(Dependabot::GitCommitChecker))
185
203
  end
186
204
 
205
+ sig do
206
+ params(
207
+ req: T::Hash[Symbol, T.untyped],
208
+ new_ref: String
209
+ ).returns(T::Hash[Symbol, T.untyped])
210
+ end
211
+ def updated_comment_version_metadata(req, new_ref)
212
+ existing_metadata = T.cast(req.fetch(:metadata, {}), T::Hash[Symbol, T.untyped])
213
+ comment = T.cast(existing_metadata[:comment], T.nilable(String))
214
+ return existing_metadata unless comment
215
+
216
+ old_version = extract_version_from_comment(comment)
217
+ return existing_metadata unless old_version
218
+
219
+ new_version = resolve_new_comment_version(new_ref)
220
+ return existing_metadata unless new_version
221
+
222
+ existing_metadata.merge(comment_version: old_version, new_comment_version: new_version)
223
+ end
224
+
225
+ sig { params(comment: String).returns(T.nilable(String)) }
226
+ def extract_version_from_comment(comment)
227
+ match = comment.match(CommentVersionHelper::COMMENT_VERSION_PATTERN)
228
+ match&.[](0)
229
+ end
230
+
231
+ sig { returns(T.nilable(Dependabot::Version)) }
232
+ def version_from_comment
233
+ @version_from_comment ||= T.let(
234
+ begin
235
+ comment = T.let(
236
+ @dependency.requirements
237
+ .filter_map do |req|
238
+ val = T.cast(req.fetch(:metadata, {}), T::Hash[Symbol, T.untyped])[:comment]
239
+ T.cast(val, T.nilable(String))
240
+ end
241
+ .first,
242
+ T.nilable(String)
243
+ )
244
+ return nil unless comment
245
+
246
+ version_string = extract_version_from_comment(comment)
247
+ return nil unless version_string
248
+
249
+ cleaned = version_string.sub(/^v/, "")
250
+ return nil unless version_class.correct?(cleaned)
251
+
252
+ version_class.new(cleaned)
253
+ end,
254
+ T.nilable(Dependabot::Version)
255
+ )
256
+ end
257
+
258
+ sig { params(_new_ref: String).returns(T.nilable(String)) }
259
+ def resolve_new_comment_version(_new_ref)
260
+ new_tag = T.must(latest_version_finder).latest_version_tag
261
+ return T.cast(new_tag.fetch(:tag, nil), T.nilable(String)) if new_tag
262
+
263
+ nil
264
+ end
265
+
187
266
  sig { returns(Dependabot::PreCommit::Helpers::Githelper) }
188
267
  def git_helper
189
268
  Helpers::Githelper.new(
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-pre_commit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.362.0
4
+ version: 0.364.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,84 +15,84 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.362.0
18
+ version: 0.364.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.362.0
25
+ version: 0.364.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dependabot-cargo
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.362.0
32
+ version: 0.364.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.362.0
39
+ version: 0.364.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: dependabot-common
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 0.362.0
46
+ version: 0.364.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 0.362.0
53
+ version: 0.364.0
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: dependabot-go_modules
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 0.362.0
60
+ version: 0.364.0
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 0.362.0
67
+ version: 0.364.0
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: dependabot-npm_and_yarn
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - '='
73
73
  - !ruby/object:Gem::Version
74
- version: 0.362.0
74
+ version: 0.364.0
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - '='
80
80
  - !ruby/object:Gem::Version
81
- version: 0.362.0
81
+ version: 0.364.0
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: dependabot-python
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.362.0
88
+ version: 0.364.0
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 0.362.0
95
+ version: 0.364.0
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: debug
98
98
  requirement: !ruby/object:Gem::Requirement
@@ -155,14 +155,14 @@ dependencies:
155
155
  requirements:
156
156
  - - "~>"
157
157
  - !ruby/object:Gem::Version
158
- version: '1.3'
158
+ version: '2.0'
159
159
  type: :development
160
160
  prerelease: false
161
161
  version_requirements: !ruby/object:Gem::Requirement
162
162
  requirements:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
- version: '1.3'
165
+ version: '2.0'
166
166
  - !ruby/object:Gem::Dependency
167
167
  name: rspec-sorbet
168
168
  requirement: !ruby/object:Gem::Requirement
@@ -314,13 +314,16 @@ files:
314
314
  - lib/dependabot/pre_commit.rb
315
315
  - lib/dependabot/pre_commit/additional_dependency_checkers.rb
316
316
  - lib/dependabot/pre_commit/additional_dependency_checkers/base.rb
317
+ - lib/dependabot/pre_commit/additional_dependency_checkers/dart.rb
317
318
  - lib/dependabot/pre_commit/additional_dependency_checkers/go.rb
318
319
  - lib/dependabot/pre_commit/additional_dependency_checkers/node.rb
319
320
  - lib/dependabot/pre_commit/additional_dependency_checkers/python.rb
320
321
  - lib/dependabot/pre_commit/additional_dependency_checkers/ruby.rb
321
322
  - lib/dependabot/pre_commit/additional_dependency_checkers/rust.rb
323
+ - lib/dependabot/pre_commit/comment_version_helper.rb
322
324
  - lib/dependabot/pre_commit/file_fetcher.rb
323
325
  - lib/dependabot/pre_commit/file_parser.rb
326
+ - lib/dependabot/pre_commit/file_parser/hook_language_fetcher.rb
324
327
  - lib/dependabot/pre_commit/file_updater.rb
325
328
  - lib/dependabot/pre_commit/helpers.rb
326
329
  - lib/dependabot/pre_commit/metadata_finder.rb
@@ -335,7 +338,7 @@ licenses:
335
338
  - MIT
336
339
  metadata:
337
340
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
338
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.362.0
341
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.364.0
339
342
  rdoc_options: []
340
343
  require_paths:
341
344
  - lib