Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bfa2c3a
Prioritize tagged releases over latest commit in git_submodules
etan-status Sep 9, 2025
500fa98
Lower number of commits to fetch
etan-status Sep 10, 2025
a21a9d6
Re-generate vcr for new commit target
etan-status Sep 10, 2025
aa58b67
Extend test coverage
etan-status Sep 10, 2025
3eff8b8
Only allow forward (or sidewards) motion when upgrading
etan-status Sep 10, 2025
f729573
Re-generate vcr as commits are only fetched while needed
etan-status Sep 10, 2025
f55f80e
Merge branch 'main' into dev/etan/gm-semver
etan-status Sep 16, 2025
559a9e3
Merge branch 'main' into dev/etan/gm-semver
etan-status Sep 17, 2025
205b1e5
Merge branch 'main' into dev/etan/gm-semver
etan-status Sep 18, 2025
36037f8
Merge branch 'main' into dev/etan/gm-semver
etan-status Sep 24, 2025
34845cc
Merge branch 'main' into dev/etan/gm-semver
etan-status Sep 29, 2025
dd53430
Merge branch 'main' into dev/etan/gm-semver
etan-status Sep 30, 2025
b03eaf3
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 1, 2025
0a048b7
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 2, 2025
30279cd
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 3, 2025
b6649ee
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 6, 2025
5d7e8f9
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 7, 2025
8c0422c
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 8, 2025
130b07b
Merge branch 'main' into dev/etan/gm-semver
v-thavaahariharangit Oct 24, 2025
c12c28a
Merge branch 'main' into dev/etan/gm-semver
etan-status Oct 30, 2025
ca32967
Merge branch 'main' into dev/etan/gm-semver
v-thavaahariharangit Nov 7, 2025
cbc8ede
Merge branch 'main' into dev/etan/gm-semver
v-thavaahariharangit Nov 13, 2025
cf9fb41
Merge branch 'main' into dev/etan/gm-semver
etan-status Dec 30, 2025
bd81da7
Update TARGET_COMMITS_TO_FETCH to 500
etan-status Jan 19, 2026
6e6fa85
Merge branch 'main' into dev/etan/gm-semver
etan-status Jan 19, 2026
aadffbb
Merge branch 'main' into dev/etan/gm-semver
etan-status Jan 19, 2026
6f21b4a
Merge branch 'main' into dev/etan/gm-semver
etan-status Jan 20, 2026
38ad962
Merge branch 'main' into dev/etan/gm-semver
v-thavaahariharangit Feb 10, 2026
790fff4
Merge branch 'main' into dev/etan/gm-semver
v-thavaahariharangit Feb 17, 2026
929cd12
Avoid changing dry-run script
etan-status Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions common/lib/dependabot/git_commit_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,32 @@ def head_commit_for_pinned_ref
local_repo_git_metadata_fetcher.head_commit_for_ref_sha(T.must(ref))
end

sig { returns(Excon::Response) }
def ref_details_for_pinned_ref
sig { returns(T::Array[GitRef]) }
def tags
GitMetadataFetcher.new(
url: dependency.source_details&.fetch(:url, nil),
credentials: credentials
).tags
end

sig { params(ref: String).returns(Excon::Response) }
def ref_details(ref)
T.must(
T.let(
GitMetadataFetcher.new(
url: dependency.source_details&.fetch(:url, nil),
credentials: credentials
).ref_details_for_pinned_ref(ref_pinned),
).ref_details_for_pinned_ref(ref),
T.nilable(Excon::Response)
)
)
end

sig { returns(Excon::Response) }
def ref_details_for_pinned_ref
ref_details(ref_pinned)
end

sig { params(ref: String).returns(T::Boolean) }
def ref_looks_like_commit_sha?(ref)
ref.match?(/^[0-9a-f]{6,40}$/)
Expand Down
3 changes: 2 additions & 1 deletion common/lib/dependabot/git_metadata_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def initialize(body:, status:)
end

KNOWN_HOSTS = /github\.com|bitbucket\.org|gitlab.com/i
MAX_COMMITS_PER_PAGE = 100

sig do
params(
Expand Down Expand Up @@ -391,7 +392,7 @@ def provider_url(ref)
github: provider_url.gsub("github.com", "api.github.com/repos")
}.freeze

"#{api_url[:github]}/commits?per_page=100&sha=#{ref}"
"#{api_url[:github]}/commits?per_page=#{MAX_COMMITS_PER_PAGE}&sha=#{ref}"
end
end
end
22 changes: 22 additions & 0 deletions common/lib/dependabot/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,28 @@ def lowest_prerelease_suffix
"a"
end

# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
SEMVER_REGEX = T.let(
/
# major.minor.patch
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)
# pre-release
(?:-(
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
))?
# build metadata
(?:\+(
[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*
))?$/x,
Regexp
)

sig { params(version: String).returns(T::Boolean) }
def self.valid_semver?(version)
!version.match(SEMVER_REGEX).nil?
end

sig { returns(T.nilable([Integer, Integer, Integer])) }
def semver_parts
# Extracts only the numeric major.minor.patch part of the version, ensuring it starts with a number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,9 @@ def initialize(dependency:, credentials:)

sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) }
def available_versions
sha_to_tags = build_sha_to_tags
versions_metadata = T.let(fetch_tags_and_release_date, T.nilable(T::Array[GitTagWithDetail]))

# as git submodules do not have versions (refs/tags are used instead), we use a pseudo version as placeholder
pseudo_version = 1.0

# we fallback to the git based tag info if no versions metadata is available
if versions_metadata&.empty?
versions_metadata = T.let(
Expand All @@ -52,15 +50,12 @@ def available_versions
)
end

releases = T.must(versions_metadata).map do |version_details|
Dependabot::Package::PackageRelease.new(
version: GitSubmodules::Version.new((pseudo_version += 1).to_s),
tag: version_details.tag,
released_at: version_details.release_date ? Time.parse(T.must(version_details.release_date)) : nil
)
end
# as git submodules do not have versions (refs/tags are used instead), we use a pseudo version as placeholder
pseudo_version = T.must(versions_metadata).length + 1

releases
T.must(versions_metadata).flat_map do |version_details|
process_metadata(version_details, sha_to_tags, pseudo_version -= 1)
end
end

private
Expand All @@ -69,10 +64,7 @@ def available_versions
def fetch_latest_tag_info
parsed_results = T.let([], T::Array[GitTagWithDetail])

git_commit_checker = Dependabot::GitCommitChecker.new(
dependency: dependency,
credentials: credentials
)
git_commit_checker = build_client

parsed_results <<
GitTagWithDetail.new(
Expand All @@ -82,49 +74,132 @@ def fetch_latest_tag_info
parsed_results
end

TARGET_COMMITS_TO_FETCH = 500
private_constant :TARGET_COMMITS_TO_FETCH

sig { returns(T::Array[GitTagWithDetail]) }
def fetch_tags_and_release_date
parsed_results = T.let([], T::Array[GitTagWithDetail])

begin
Dependabot.logger.info("Fetching release info for Git Submodules: #{dependency.name}")

client = Dependabot::GitCommitChecker.new(
dependency: dependency,
credentials: credentials
)

response = client.ref_details_for_pinned_ref

unless response.status == 200
Dependabot.logger.error(
"Error while fetching details for #{dependency.name} " \
"Detail : #{response.body}"
)
client = build_client

sha = T.let(nil, T.nilable(String))
catch :found do
while parsed_results.length < TARGET_COMMITS_TO_FETCH
max_len = Dependabot::GitMetadataFetcher::MAX_COMMITS_PER_PAGE
max_len -= 1 unless sha.nil?
commits = get_commits(client, sha)
break if commits.empty?

commits.each do |commit|
sha = commit["sha"]
parsed_results << GitTagWithDetail.new(
tag: sha,
release_date: commit["commit"]["committer"]["date"]
)
throw :found if sha == dependency.version
end
break if commits.length < max_len
end
end

return parsed_results unless response.status == 200

releases = JSON.parse(response.body)

parsed_results = releases.map do |release|
GitTagWithDetail.new(
tag: release["sha"],
release_date: release["commit"]["committer"]["date"]
)
end

parsed_results
rescue StandardError => e
Dependabot.logger.error("Error while fetching package info for git submodule: #{e.message}")
parsed_results
end
end

sig { returns(Dependabot::GitCommitChecker) }
def build_client
Dependabot::GitCommitChecker.new(
dependency: dependency,
credentials: credentials
)
end

sig { returns(String) }
def url
dependency.source_details&.fetch(:url, nil)
end

sig { returns(T::Hash[String, T::Array[String]]) }
def build_sha_to_tags
build_client.tags.each_with_object({}) do |tag, sha_to_tags|
(sha_to_tags[tag.commit_sha] ||= []) << tag.name
end
end

sig do
params(
client: Dependabot::GitCommitChecker,
sha: T.nilable(String)
).returns(T::Array[T::Hash[String, T.untyped]])
end
def get_commits(client, sha)
response = sha.nil? ? client.ref_details_for_pinned_ref : client.ref_details(sha)

unless response.status == 200
Dependabot.logger.error(
"Error while fetching details for #{dependency.name} " \
"Detail : #{response.body}"
)
end

return [] unless response.status == 200

commits = JSON.parse(response.body)
sha.nil? || commits.empty? ? commits : commits[1..]
end

sig do
params(
version_details: GitTagWithDetail,
sha_to_tags: T::Hash[String, T::Array[String]],
pseudo_version: Integer
).returns(T::Array[Dependabot::Package::PackageRelease])
end
def process_metadata(version_details, sha_to_tags, pseudo_version)
released_at = version_details.release_date ? Time.parse(T.must(version_details.release_date)) : nil
sha = version_details.tag

normalized_versions(sha, sha_to_tags, pseudo_version).map do |version|
Dependabot::Package::PackageRelease.new(
version: version,
tag: sha,
released_at: released_at
)
end
end

sig do
params(
sha: String,
sha_to_tags: T::Hash[String, T::Array[String]],
pseudo_version: Integer
).returns(T::Array[Dependabot::Version])
end
def normalized_versions(sha, sha_to_tags, pseudo_version)
versions = Array(sha_to_tags[sha]).map do |tag_name|
normalized_version(tag_name, pseudo_version)
end

versions << normalized_version(sha, pseudo_version)

versions.uniq
end

sig { params(tag: String, pseudo_version: Integer).returns(Dependabot::Version) }
def normalized_version(tag, pseudo_version)
if Dependabot::Version.valid_semver?(tag)
Dependabot::Version.new(tag)
elsif tag.start_with?("v") && GitSubmodules::Version.valid_semver?(T.must(tag[1..]))
Dependabot::Version.new(tag[1..])
else
Dependabot::Version.new("0.0.0-0.#{pseudo_version}")
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def latest_tag

# if there are no releases after applying filters, we fallback to the current tag to avoid empty results
releases = apply_post_fetch_latest_versions_filter(releases)
releases.first&.tag
releases.max_by(&:version)&.tag
end

sig { returns(T.nilable(T::Array[Dependabot::Package::PackageRelease])) }
Expand Down Expand Up @@ -109,7 +109,7 @@ def apply_post_fetch_latest_versions_filter(releases)
end

releases << Dependabot::Package::PackageRelease.new(
version: GitSubmodules::Version.new("1.0.0"),
version: GitSubmodules::Version.new("0.0.0-0.0"), # Lower than versions from package_details_fetcher
tag: dependency.version
)

Expand Down
Loading
Loading