dependabot-apm 0.400.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 +7 -0
- data/lib/dependabot/apm/file_fetcher.rb +95 -0
- data/lib/dependabot/apm/file_parser.rb +313 -0
- data/lib/dependabot/apm/file_updater.rb +174 -0
- data/lib/dependabot/apm/git_commit_checker.rb +91 -0
- data/lib/dependabot/apm/metadata_finder.rb +91 -0
- data/lib/dependabot/apm/package_manager.rb +75 -0
- data/lib/dependabot/apm/package_specifier.rb +347 -0
- data/lib/dependabot/apm/requirement.rb +68 -0
- data/lib/dependabot/apm/update_checker.rb +303 -0
- data/lib/dependabot/apm/version.rb +228 -0
- data/lib/dependabot/apm.rb +28 -0
- metadata +278 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/metadata_finders"
|
|
7
|
+
require "dependabot/metadata_finders/base"
|
|
8
|
+
require "dependabot/source"
|
|
9
|
+
require "dependabot/apm/package_specifier"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Apm
|
|
13
|
+
class MetadataFinder < Dependabot::MetadataFinders::Base
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
# Hosts whose canonical git provider lets us rebuild the Source from the
|
|
17
|
+
# full repository path. Dependabot::Source.from_url caps the repo at three
|
|
18
|
+
# path segments and is not end-anchored, so a deep GitLab namespace such as
|
|
19
|
+
# `group/subgroup/team/project` would be truncated to `group/subgroup/team`
|
|
20
|
+
# and resolve to the wrong source/changelog. APM already knows the complete
|
|
21
|
+
# path via PackageSpecifier, so we bypass that regex for these hosts.
|
|
22
|
+
PROVIDER_BY_HOST = T.let(
|
|
23
|
+
{
|
|
24
|
+
"github.com" => "github",
|
|
25
|
+
"gitlab.com" => "gitlab",
|
|
26
|
+
"bitbucket.org" => "bitbucket"
|
|
27
|
+
}.freeze,
|
|
28
|
+
T::Hash[String, String]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# The API path each provider serves under its own authority. A dependency
|
|
32
|
+
# may pin a custom port (`github.com:8443`), which is a distinct endpoint
|
|
33
|
+
# from the public SaaS host: it is a self-hosted-style install whose API is
|
|
34
|
+
# served from the same authority (mirroring Dependabot::Source's GitHub
|
|
35
|
+
# Enterprise `…/api/v3` handling), not from the public `api.github.com`.
|
|
36
|
+
# Used only when a port is present; portless canonical hosts keep the
|
|
37
|
+
# provider's public defaults.
|
|
38
|
+
API_PATH_BY_PROVIDER = T.let(
|
|
39
|
+
{
|
|
40
|
+
"github" => "api/v3",
|
|
41
|
+
"gitlab" => "api/v4",
|
|
42
|
+
"bitbucket" => "2.0"
|
|
43
|
+
}.freeze,
|
|
44
|
+
T::Hash[String, String]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
sig { override.returns(T.nilable(Dependabot::Source)) }
|
|
50
|
+
def look_up_source
|
|
51
|
+
url = dependency.requirements.first&.source_string("url")
|
|
52
|
+
return unless url
|
|
53
|
+
|
|
54
|
+
source_from_full_path(url) || Source.from_url(url)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Builds the Source directly from the full repository path so nested
|
|
58
|
+
# namespaces survive, falling back (via the caller) to Source.from_url for
|
|
59
|
+
# hosts we cannot resolve a provider for (e.g. self-hosted installs).
|
|
60
|
+
sig { params(url: String).returns(T.nilable(Dependabot::Source)) }
|
|
61
|
+
def source_from_full_path(url)
|
|
62
|
+
spec = Dependabot::Apm::PackageSpecifier.parse(url)
|
|
63
|
+
return unless spec
|
|
64
|
+
|
|
65
|
+
# Classify by the hostname alone: a custom port is transport, not a
|
|
66
|
+
# different provider, so `github.com:8443` is still GitHub.
|
|
67
|
+
hostname = Dependabot::Apm::PackageSpecifier.hostname_without_port(spec.host)
|
|
68
|
+
provider = PROVIDER_BY_HOST[hostname]
|
|
69
|
+
return if provider.nil? || spec.repo.empty?
|
|
70
|
+
|
|
71
|
+
repo = "#{spec.owner}/#{spec.repo}"
|
|
72
|
+
# A portless canonical host uses the provider's public defaults.
|
|
73
|
+
return Dependabot::Source.new(provider: provider, repo: repo) if spec.host == hostname
|
|
74
|
+
|
|
75
|
+
# A custom port is a distinct, self-hosted-style endpoint: preserve the
|
|
76
|
+
# full authority and its matching API so source and changelog links
|
|
77
|
+
# target it. Passing the URL to Source.from_url instead would misread the
|
|
78
|
+
# port as part of the repo path (`github.com:8443/org/repo` -> `8443/org`).
|
|
79
|
+
Dependabot::Source.new(
|
|
80
|
+
provider: provider,
|
|
81
|
+
repo: repo,
|
|
82
|
+
hostname: spec.host,
|
|
83
|
+
api_endpoint: "https://#{spec.host}/#{API_PATH_BY_PROVIDER.fetch(provider)}"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Dependabot::MetadataFinders
|
|
91
|
+
.register("apm", Dependabot::Apm::MetadataFinder)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/ecosystem"
|
|
6
|
+
require "dependabot/version"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Apm
|
|
10
|
+
ECOSYSTEM = "apm"
|
|
11
|
+
PACKAGE_MANAGER = "apm"
|
|
12
|
+
|
|
13
|
+
# The default version reported when the resolved apm CLI version cannot be
|
|
14
|
+
# determined from the lockfile (`apm_version`).
|
|
15
|
+
DEFAULT_PACKAGE_MANAGER_VERSION = "0.0.0"
|
|
16
|
+
|
|
17
|
+
SUPPORTED_APM_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
|
18
|
+
DEPRECATED_APM_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
|
19
|
+
|
|
20
|
+
class PackageManager < Dependabot::Ecosystem::VersionManager
|
|
21
|
+
extend T::Sig
|
|
22
|
+
|
|
23
|
+
# Matches the top-level `apm_version` key the lockfile records for the apm
|
|
24
|
+
# CLI that wrote it. The value may be plain or quoted, so optional quotes
|
|
25
|
+
# are stripped. Nested `version:` keys under `dependencies:` are indented
|
|
26
|
+
# and so never match this line-anchored pattern.
|
|
27
|
+
LOCKFILE_VERSION_REGEX = /^apm_version:\s*['"]?(?<version>[^'"\s]+)['"]?\s*$/
|
|
28
|
+
|
|
29
|
+
sig { params(raw_version: String).void }
|
|
30
|
+
def initialize(raw_version)
|
|
31
|
+
super(
|
|
32
|
+
name: PACKAGE_MANAGER,
|
|
33
|
+
version: PackageManager.parse_version(raw_version),
|
|
34
|
+
deprecated_versions: DEPRECATED_APM_VERSIONS,
|
|
35
|
+
supported_versions: SUPPORTED_APM_VERSIONS
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The apm CLI version recorded in the lockfile (`apm_version`), or nil when
|
|
40
|
+
# there is no lockfile or it does not record one. Read straight from the
|
|
41
|
+
# raw content rather than by parsing the YAML: the lockfile is a support
|
|
42
|
+
# file we never rewrite, and a malformed one should degrade to the default
|
|
43
|
+
# version rather than raise while parsing the project.
|
|
44
|
+
sig { params(lockfile_content: T.nilable(String)).returns(T.nilable(String)) }
|
|
45
|
+
def self.version_from_lockfile(lockfile_content)
|
|
46
|
+
lockfile_content&.match(LOCKFILE_VERSION_REGEX)&.[](:version)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The apm CLI version recorded in the lockfile (`apm_version`) is written
|
|
50
|
+
# by apm from its Python (PEP 440) distribution metadata, and the lockfile
|
|
51
|
+
# schema permits an arbitrary string, so it is not necessarily strict
|
|
52
|
+
# SemVer (e.g. `0.32.0rc1`, `0.32.0.dev3`). Parse it with the permissive
|
|
53
|
+
# base version rather than the strict Apm::Version — which is meant for
|
|
54
|
+
# dependency git refs and would raise ArgumentError here — falling back to
|
|
55
|
+
# the default when the string cannot be represented at all, so an unusual
|
|
56
|
+
# CLI version never prevents parsing the project.
|
|
57
|
+
sig { params(raw_version: String).returns(Dependabot::Version) }
|
|
58
|
+
def self.parse_version(raw_version)
|
|
59
|
+
return Dependabot::Version.new(raw_version) if Dependabot::Version.correct?(raw_version)
|
|
60
|
+
|
|
61
|
+
Dependabot::Version.new(DEFAULT_PACKAGE_MANAGER_VERSION)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { returns(T::Boolean) }
|
|
65
|
+
def deprecated?
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { returns(T::Boolean) }
|
|
70
|
+
def unsupported?
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module Apm
|
|
8
|
+
# Parses a single `dependencies.apm` entry from an `apm.yml` manifest into its
|
|
9
|
+
# git coordinates. APM supports several equivalent forms for the same entry:
|
|
10
|
+
#
|
|
11
|
+
# owner/repo GitHub shorthand, default branch
|
|
12
|
+
# owner/repo#v1.0.0 Pinned to a tag, branch or SHA
|
|
13
|
+
# gitlab.com/acme/repo#v2.0 FQDN shorthand (any git host)
|
|
14
|
+
# gitlab.com/group/subgroup/repo#v2.0 Nested subgroup repo (non-GitHub)
|
|
15
|
+
# owner/repo/skills/review Virtual subdirectory of a repo
|
|
16
|
+
# owner/repo/prompts/x.prompt.md#v1.0.0 Virtual file within a repo
|
|
17
|
+
# https://gitlab.com/acme/repo.git Explicit HTTPS git URL
|
|
18
|
+
# git@gitlab.com:acme/repo.git SSH SCP-style URL
|
|
19
|
+
# ssh://git@gitlab.com/acme/repo.git SSH URI-style URL
|
|
20
|
+
#
|
|
21
|
+
# HTTPS and SSH clone URLs both resolve to an `https://host[:port]/owner/repo`
|
|
22
|
+
# remote, since Dependabot enumerates tags over HTTPS with a token. An
|
|
23
|
+
# `https://` source keeps its authority, including a non-default port
|
|
24
|
+
# (`github.com:8443` is a distinct endpoint); the implicit `:443` is
|
|
25
|
+
# normalised away so it collapses to the same endpoint as a portless
|
|
26
|
+
# declaration. An `ssh://` (or `git@host:`
|
|
27
|
+
# SCP) source reaches the same repositories over HTTPS, so it resolves there
|
|
28
|
+
# and its SSH port (e.g. `:2222`) is dropped, as it is not an HTTPS port.
|
|
29
|
+
# Host classification (GitHub-family, Azure DevOps) always ignores the port.
|
|
30
|
+
# `http://` and `git://` URLs name a different endpoint than HTTPS and are
|
|
31
|
+
# not silently rewritten; they are out of scope for v1 and resolve to `nil`.
|
|
32
|
+
#
|
|
33
|
+
# Virtual package paths (`owner/repo/skills/review`) are a GitHub-family
|
|
34
|
+
# shorthand, since GitHub repositories are always `owner/repo`. That covers
|
|
35
|
+
# github.com and GitHub Enterprise Cloud data-residency hosts (`*.ghe.com`),
|
|
36
|
+
# which APM also resolves as GitHub. On other hosts (e.g. GitLab) the whole
|
|
37
|
+
# path is treated as the repository so nested subgroups resolve to the
|
|
38
|
+
# correct remote; virtual packages there are out of scope for v1, as is
|
|
39
|
+
# self-hosted GHES on an arbitrary hostname that cannot be recognised from
|
|
40
|
+
# the host alone.
|
|
41
|
+
#
|
|
42
|
+
# Azure DevOps hosts (`dev.azure.com`, `ssh.dev.azure.com` and legacy
|
|
43
|
+
# `*.visualstudio.com`) expose repositories at `org/project/_git/repo`, a
|
|
44
|
+
# structure this generic `owner/repo` builder cannot construct, so their
|
|
45
|
+
# entries are out of scope for v1 and resolve to `nil`.
|
|
46
|
+
#
|
|
47
|
+
# Local path entries (`./pkg`, `../pkg`, `/pkg`, `~/pkg`, and their Windows
|
|
48
|
+
# `.\`/`..\`/`~\` forms) are not versioned by a remote git host and resolve
|
|
49
|
+
# to `nil`, as do entries we cannot confidently parse.
|
|
50
|
+
class PackageSpecifier
|
|
51
|
+
extend T::Sig
|
|
52
|
+
|
|
53
|
+
DEFAULT_HOST = "github.com"
|
|
54
|
+
|
|
55
|
+
# Azure DevOps Services hosts. Their repositories live at
|
|
56
|
+
# `org/project/_git/repo`, a structure this generic owner/repo builder
|
|
57
|
+
# cannot express, so ADO entries are out of scope for v1 (see README) and
|
|
58
|
+
# are skipped rather than resolved to a wrong remote. Legacy
|
|
59
|
+
# per-organisation hosts match the `.visualstudio.com` suffix separately.
|
|
60
|
+
AZURE_DEVOPS_HOSTS = %w(dev.azure.com ssh.dev.azure.com).freeze
|
|
61
|
+
|
|
62
|
+
# SCP-style SSH shorthand `user@host:path`. APM accepts any valid SSH
|
|
63
|
+
# username here (not only `git@`), so match any user that has no `:` or
|
|
64
|
+
# `/` (either would signal a URL scheme or path rather than an SCP user).
|
|
65
|
+
# The user is discarded — only the host and path determine the git
|
|
66
|
+
# coordinates, just as URL_STYLE drops its `user@` info.
|
|
67
|
+
SCP_STYLE = %r{\A(?<user>[^@:/\s]+)@(?<host>[^:/\s]+):(?<path>.+)\z}
|
|
68
|
+
# Matches https/git/ssh URIs, discarding any `user@` info (e.g. the
|
|
69
|
+
# `git@` in an `ssh://git@host/owner/repo` URL) so only the host remains.
|
|
70
|
+
# URI schemes are case-insensitive (RFC 3986), so the scheme alternation
|
|
71
|
+
# is matched case-insensitively and normalised (downcased) before dispatch;
|
|
72
|
+
# e.g. `HTTPS://host/owner/repo` is recognised rather than mis-parsed as a
|
|
73
|
+
# default-host shorthand.
|
|
74
|
+
URL_STYLE = %r{\A(?<scheme>(?i:https?|git|ssh))://(?:[^@/]+@)?(?<host>[^/]+)/(?<path>.+)\z}
|
|
75
|
+
|
|
76
|
+
sig { returns(String) }
|
|
77
|
+
attr_reader :host
|
|
78
|
+
|
|
79
|
+
sig { returns(String) }
|
|
80
|
+
attr_reader :owner
|
|
81
|
+
|
|
82
|
+
sig { returns(String) }
|
|
83
|
+
attr_reader :repo
|
|
84
|
+
|
|
85
|
+
sig { returns(T.nilable(String)) }
|
|
86
|
+
attr_reader :sub_path
|
|
87
|
+
|
|
88
|
+
sig { returns(T.nilable(String)) }
|
|
89
|
+
attr_reader :ref
|
|
90
|
+
|
|
91
|
+
sig { params(raw: Object, default_host: String).returns(T.nilable(Dependabot::Apm::PackageSpecifier)) }
|
|
92
|
+
def self.parse(raw, default_host: DEFAULT_HOST)
|
|
93
|
+
return nil unless raw.is_a?(String)
|
|
94
|
+
|
|
95
|
+
entry = raw.strip
|
|
96
|
+
return nil if entry.empty? || local_path?(entry)
|
|
97
|
+
|
|
98
|
+
spec, _, ref = entry.partition("#")
|
|
99
|
+
host, path = split_host_and_path(spec, default_host)
|
|
100
|
+
return nil unless path
|
|
101
|
+
|
|
102
|
+
build(host: host, path: path, ref: ref, default_host: default_host)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# True when `raw` is the string shorthand form (`[host/]owner/repo…`)
|
|
106
|
+
# rather than an explicit git reference (`https://`, `http://`, `ssh://git@`,
|
|
107
|
+
# an SCP `git@host:path`, or any `.git`-suffixed ref). When an apm manifest
|
|
108
|
+
# configures a default registry, APM routes shorthand entries through it
|
|
109
|
+
# instead of Git, so the parser skips them (registry dependencies are out
|
|
110
|
+
# of scope for v1). This mirrors APM's `_is_explicit_git_form`, which
|
|
111
|
+
# routes URL, SCP and `.git` forms to Git even when a default registry is
|
|
112
|
+
# configured -- they are the escape hatch and are never registry-routed.
|
|
113
|
+
sig { params(raw: Object).returns(T::Boolean) }
|
|
114
|
+
def self.shorthand?(raw)
|
|
115
|
+
return false unless raw.is_a?(String)
|
|
116
|
+
|
|
117
|
+
spec = raw.strip.partition("#").first.to_s
|
|
118
|
+
return false if spec.empty? || local_path?(spec)
|
|
119
|
+
|
|
120
|
+
# A `.git` suffix marks an explicit git reference (as APM's
|
|
121
|
+
# `_is_explicit_git_form` treats it), so it is a Git escape hatch rather
|
|
122
|
+
# than a registry-routed shorthand even without a transport scheme.
|
|
123
|
+
return false if spec.downcase.end_with?(".git")
|
|
124
|
+
|
|
125
|
+
!spec.match?(SCP_STYLE) && !spec.match?(URL_STYLE)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig do
|
|
129
|
+
params(host: String, path: String, ref: String, default_host: String)
|
|
130
|
+
.returns(T.nilable(Dependabot::Apm::PackageSpecifier))
|
|
131
|
+
end
|
|
132
|
+
def self.build(host:, path:, ref:, default_host: DEFAULT_HOST)
|
|
133
|
+
# DNS hostnames are case-insensitive, so canonicalise to lowercase once
|
|
134
|
+
# here; repository splitting, naming and credential-host matching all key
|
|
135
|
+
# off `host` and must agree on e.g. `GitHub.com` == `github.com`.
|
|
136
|
+
host = host.downcase
|
|
137
|
+
# Azure DevOps clone URLs are `org/project/_git/repo`, which this generic
|
|
138
|
+
# owner/repo builder cannot construct, so ADO shorthands and URLs are not
|
|
139
|
+
# versioned in v1 (see README). Skip them rather than emit a wrong remote
|
|
140
|
+
# that GitCommitChecker would query as a non-repository endpoint.
|
|
141
|
+
return nil if azure_devops_host?(host)
|
|
142
|
+
|
|
143
|
+
segments = path.delete_suffix(".git").split("/").reject(&:empty?)
|
|
144
|
+
owner = segments[0]
|
|
145
|
+
return nil if owner.nil? || segments[1].nil?
|
|
146
|
+
|
|
147
|
+
repo, sub_path = repository_and_sub_path(host, segments)
|
|
148
|
+
|
|
149
|
+
# GitHub-family owner/repo paths are case-insensitive, so canonicalise
|
|
150
|
+
# them to lowercase to give each repository a single stable identity.
|
|
151
|
+
# Other hosts (e.g. GitLab) are case-sensitive and MUST preserve casing,
|
|
152
|
+
# as must virtual sub paths (they address entries inside the repo tree).
|
|
153
|
+
if github_family?(host)
|
|
154
|
+
owner = owner.downcase
|
|
155
|
+
repo = repo.downcase
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
new(
|
|
159
|
+
host: host,
|
|
160
|
+
owner: owner,
|
|
161
|
+
repo: repo,
|
|
162
|
+
sub_path: sub_path.empty? ? nil : sub_path,
|
|
163
|
+
ref: ref.empty? ? nil : ref,
|
|
164
|
+
default_host: default_host.downcase
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Splits the path segments (after `owner`) into a repository path and an
|
|
169
|
+
# optional virtual sub path. GitHub-family hosts (github.com and GitHub
|
|
170
|
+
# Enterprise Cloud `*.ghe.com`) always expose repositories as exactly
|
|
171
|
+
# `owner/repo`, so any deeper segments are an APM virtual package path.
|
|
172
|
+
# Other hosts (e.g. GitLab) allow repositories nested at arbitrary subgroup
|
|
173
|
+
# depth, and APM resolves the repository/virtual boundary host-specifically
|
|
174
|
+
# rather than from the path alone; to avoid querying a shallower, wrong
|
|
175
|
+
# remote we keep the whole path as the repository there. Virtual packages
|
|
176
|
+
# outside the GitHub family are therefore out of scope for v1.
|
|
177
|
+
sig { params(host: String, segments: T::Array[String]).returns([String, String]) }
|
|
178
|
+
def self.repository_and_sub_path(host, segments)
|
|
179
|
+
if github_family?(host)
|
|
180
|
+
[T.must(segments[1]), (segments[2..] || []).join("/")]
|
|
181
|
+
else
|
|
182
|
+
[(segments[1..] || []).join("/"), ""]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# GitHub-family hosts share the strict `owner/repo` boundary (any deeper
|
|
187
|
+
# segments are an APM virtual package path) and case-insensitive owner and
|
|
188
|
+
# repo names. That is github.com plus GitHub Enterprise Cloud
|
|
189
|
+
# data-residency hosts (`*.ghe.com`), which APM resolves as GitHub too.
|
|
190
|
+
# Self-hosted GHES uses arbitrary hostnames that cannot be recognised from
|
|
191
|
+
# the host alone, so it is not detected here -- a follow-up for once host
|
|
192
|
+
# configuration is threaded through the parser.
|
|
193
|
+
sig { params(host: String).returns(T::Boolean) }
|
|
194
|
+
def self.github_family?(host)
|
|
195
|
+
hostname = hostname_without_port(host)
|
|
196
|
+
hostname == DEFAULT_HOST || hostname.end_with?(".ghe.com")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# True for Azure DevOps Services hosts: dev.azure.com, its SSH alias, and
|
|
200
|
+
# legacy per-organisation `*.visualstudio.com` hosts. APM special-cases
|
|
201
|
+
# these with `org/project/_git/repo` clone URLs; v1 does not build those,
|
|
202
|
+
# so such entries are skipped (see README) rather than resolved to a wrong
|
|
203
|
+
# remote.
|
|
204
|
+
sig { params(host: String).returns(T::Boolean) }
|
|
205
|
+
def self.azure_devops_host?(host)
|
|
206
|
+
hostname = hostname_without_port(host)
|
|
207
|
+
AZURE_DEVOPS_HOSTS.include?(hostname) || hostname.end_with?(".visualstudio.com")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The hostname with any `:port` suffix removed. Host-family classification
|
|
211
|
+
# keys off the hostname alone: the port is part of the authority that
|
|
212
|
+
# `git_url` and `name` keep, but it must not change which family a URL
|
|
213
|
+
# belongs to (`github.com:8443` is still GitHub, `foo.ghe.com:8443` still
|
|
214
|
+
# GHE Cloud). Only a trailing `:<digits>` is stripped, so bracketed IPv6
|
|
215
|
+
# authorities are left intact.
|
|
216
|
+
sig { params(host: String).returns(String) }
|
|
217
|
+
def self.hostname_without_port(host)
|
|
218
|
+
host.sub(/:\d+\z/, "")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The authority with a default HTTPS port removed. APM treats a
|
|
222
|
+
# non-default port as part of a dependency's identity but normalises the
|
|
223
|
+
# implicit `:443` away (its `_split_shorthand_host_port` maps port 443 to
|
|
224
|
+
# "no port"), so `github.com:443` and `github.com` are the same endpoint
|
|
225
|
+
# while `github.com:8443` stays distinct. Applied when parsing an HTTPS
|
|
226
|
+
# URL so `git_url` and `name` share one canonical authority; only a
|
|
227
|
+
# trailing `:<digits>` is considered, leaving bracketed IPv6 hosts intact.
|
|
228
|
+
sig { params(host: String).returns(String) }
|
|
229
|
+
def self.normalize_https_port(host)
|
|
230
|
+
match = host.match(/\A(?<hostname>.+):(?<port>\d+)\z/)
|
|
231
|
+
return host unless match
|
|
232
|
+
|
|
233
|
+
T.must(match[:port]).to_i == 443 ? T.must(match[:hostname]) : host
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
sig { params(entry: String).returns(T::Boolean) }
|
|
237
|
+
def self.local_path?(entry)
|
|
238
|
+
entry.start_with?("./", "../", "/", "~/", ".\\", "..\\", "~\\") ||
|
|
239
|
+
entry == "." || entry == "~"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
sig { params(spec: String, default_host: String).returns([String, T.nilable(String)]) }
|
|
243
|
+
def self.split_host_and_path(spec, default_host)
|
|
244
|
+
if (m = spec.match(SCP_STYLE))
|
|
245
|
+
return [T.must(m[:host]), m[:path]]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
if (m = spec.match(URL_STYLE))
|
|
249
|
+
host = T.must(m[:host])
|
|
250
|
+
case T.must(m[:scheme]).downcase
|
|
251
|
+
when "https"
|
|
252
|
+
# HTTPS is the transport we query, so keep the authority, including
|
|
253
|
+
# a non-default port: `github.com:8443` is a distinct endpoint from
|
|
254
|
+
# `github.com` and must stay that way. The implicit `:443` is
|
|
255
|
+
# normalised off so a `github.com:443` declaration collapses to the
|
|
256
|
+
# same endpoint and identity as a portless one, matching APM's
|
|
257
|
+
# default-port handling.
|
|
258
|
+
return [normalize_https_port(host), m[:path]]
|
|
259
|
+
when "ssh"
|
|
260
|
+
# SSH reaches the same repositories as HTTPS on that host, and
|
|
261
|
+
# Dependabot enumerates tags over HTTPS with a token, so resolve
|
|
262
|
+
# ssh:// URIs over HTTPS. The SSH port (e.g. the `:2222` on
|
|
263
|
+
# `ssh://git@host:2222/...`) is not an HTTPS port, so drop it.
|
|
264
|
+
return [host.sub(/:\d+\z/, ""), m[:path]]
|
|
265
|
+
else
|
|
266
|
+
# http:// and git:// name a different endpoint than HTTPS (a
|
|
267
|
+
# distinct port, and for http an unencrypted service). We only query
|
|
268
|
+
# over HTTPS, so rather than silently rewrite them to a possibly
|
|
269
|
+
# wrong remote we treat them as out of scope for v1 (see README);
|
|
270
|
+
# returning a nil path makes `parse` yield nil.
|
|
271
|
+
return [host, nil]
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
first_segment = spec.split("/").first.to_s
|
|
276
|
+
# A dot in the first path segment marks an FQDN host (GitHub owners never
|
|
277
|
+
# contain a dot), e.g. `gitlab.com/acme/repo`. Normalise the HTTPS port
|
|
278
|
+
# the same way as the explicit-URL branch so `github.com:443/org/repo`
|
|
279
|
+
# shorthand collapses to the same authority and identity as the portless
|
|
280
|
+
# and explicit-URL forms (a non-default port is kept as a distinct
|
|
281
|
+
# endpoint).
|
|
282
|
+
if first_segment.include?(".")
|
|
283
|
+
[normalize_https_port(first_segment), spec.split("/")[1..].to_a.join("/")]
|
|
284
|
+
else
|
|
285
|
+
[default_host, spec]
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
sig do
|
|
290
|
+
params(
|
|
291
|
+
host: String,
|
|
292
|
+
owner: String,
|
|
293
|
+
repo: String,
|
|
294
|
+
sub_path: T.nilable(String),
|
|
295
|
+
ref: T.nilable(String),
|
|
296
|
+
default_host: String
|
|
297
|
+
).void
|
|
298
|
+
end
|
|
299
|
+
def initialize(host:, owner:, repo:, sub_path: nil, ref: nil, default_host: DEFAULT_HOST)
|
|
300
|
+
@host = host
|
|
301
|
+
@owner = owner
|
|
302
|
+
@repo = repo
|
|
303
|
+
@sub_path = sub_path
|
|
304
|
+
@ref = ref
|
|
305
|
+
@default_host = default_host
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
sig { returns(String) }
|
|
309
|
+
def git_url
|
|
310
|
+
"https://#{host}/#{owner}/#{repo}"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The dependency name shown to users, which is also APM's dedup/lock
|
|
314
|
+
# identity. Repositories on the manifest's default host -- the configured
|
|
315
|
+
# `default_host`, or github.com when unset -- keep the familiar
|
|
316
|
+
# `owner/repo` shorthand, matching how APM keys the dependency;
|
|
317
|
+
# repositories on any other host are namespaced by host to stay unique.
|
|
318
|
+
# Comparing against the effective default (rather than a hard-coded
|
|
319
|
+
# github.com) means a manifest-selected `default_host` is stripped too, so
|
|
320
|
+
# dependency-name ignore rules and deduplication use the same identity APM
|
|
321
|
+
# does. A non-default port is part of the identity, matching APM's
|
|
322
|
+
# get_identity (`git.example.com:8443/org/repo` and
|
|
323
|
+
# `git.example.com:9443/org/repo` stay distinct dependencies rather than
|
|
324
|
+
# collapse into one entry that DependencySet would merge -- which, holding
|
|
325
|
+
# two different clone URLs, then raises `Multiple sources!`). The default
|
|
326
|
+
# `:443` is normalised off at parse time, so `git_url` and `name` share
|
|
327
|
+
# one authority and a `:443` declaration dedups with a portless one. A
|
|
328
|
+
# virtual package (sub path) is namespaced by that path too: APM keys
|
|
329
|
+
# virtual packages by repository plus path, so `org/mono/skills/review`
|
|
330
|
+
# and `org/mono/skills/security` must remain distinct dependencies rather
|
|
331
|
+
# than collapse into one `org/mono` entry that DependencySet would
|
|
332
|
+
# deduplicate.
|
|
333
|
+
sig { returns(String) }
|
|
334
|
+
def name
|
|
335
|
+
identity_host = self.class.hostname_without_port(host)
|
|
336
|
+
# `host` carries only a non-default port here (the default :443 is
|
|
337
|
+
# normalised off when parsing the URL), so it is the identity authority:
|
|
338
|
+
# on the default host with no port the bare `owner/repo` shorthand is
|
|
339
|
+
# used, otherwise the port-bearing authority is prefixed.
|
|
340
|
+
on_default_host = identity_host == @default_host && host == identity_host
|
|
341
|
+
repo_name = on_default_host ? "#{owner}/#{repo}" : "#{host}/#{owner}/#{repo}"
|
|
342
|
+
virtual_path = sub_path
|
|
343
|
+
virtual_path ? "#{repo_name}/#{virtual_path}" : repo_name
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/apm/version"
|
|
7
|
+
require "dependabot/requirement"
|
|
8
|
+
require "dependabot/utils"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Apm
|
|
12
|
+
class Requirement < Dependabot::Requirement
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
# Requirement bounds are matched with SemVer 2.0.0 precedence, to stay
|
|
16
|
+
# consistent with Apm::Version. Left to Gem::Requirement, a prerelease
|
|
17
|
+
# bound such as `1.0.0-alpha.beta` is rewritten to the RubyGems form
|
|
18
|
+
# `1.0.0.pre.alpha.beta` and compared with RubyGems precedence, which
|
|
19
|
+
# orders prerelease identifiers the opposite way to SemVer (so a range
|
|
20
|
+
# like `< 1.0.0-alpha.beta` would wrongly exclude `1.0.0-alpha.1`).
|
|
21
|
+
quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
|
|
22
|
+
# Gem::Version::VERSION_PATTERN covers the release and `-prerelease`
|
|
23
|
+
# portions; the trailing group additionally accepts SemVer `+build`
|
|
24
|
+
# metadata so bounds like `= 1.0.0+build.5` are recognised too.
|
|
25
|
+
version_pattern = "#{Gem::Version::VERSION_PATTERN}(?:\\+[0-9A-Za-z][0-9A-Za-z.-]*)?"
|
|
26
|
+
|
|
27
|
+
PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze, String)
|
|
28
|
+
PATTERN = /\A#{PATTERN_RAW}\z/
|
|
29
|
+
|
|
30
|
+
# Builds each requirement bound from an Apm::Version so prerelease
|
|
31
|
+
# ordering follows SemVer. Operands that are not strict SemVer (e.g. the
|
|
32
|
+
# partial `>= 1.0` or `>= 0`) fall back to RubyGems parsing, which does
|
|
33
|
+
# not have the prerelease-ordering problem.
|
|
34
|
+
sig { override.params(obj: T.any(Gem::Version, String)).returns([String, Gem::Version]) }
|
|
35
|
+
def self.parse(obj)
|
|
36
|
+
if obj.is_a?(String) && (matches = PATTERN.match(obj)) && Apm::Version.correct?(T.must(matches[2]))
|
|
37
|
+
return [matches[1] || "=", Apm::Version.new(T.must(matches[2]))]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# For consistency with other languages, we define a requirements array.
|
|
44
|
+
# APM doesn't have an `OR` separator for requirements, so it always
|
|
45
|
+
# contains a single element.
|
|
46
|
+
sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
|
|
47
|
+
def self.requirements_array(requirement_string)
|
|
48
|
+
[new(requirement_string)]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Patches Gem::Requirement to make it accept requirement strings like
|
|
52
|
+
# "~> 4.2.5, >= 4.2.5.1" without first needing to split them.
|
|
53
|
+
sig { params(requirements: T.any(T.nilable(String), T::Array[T.nilable(String)])).void }
|
|
54
|
+
def initialize(*requirements)
|
|
55
|
+
requirements = requirements.flatten.flat_map do |req_string|
|
|
56
|
+
req_string&.split(",")&.map(&:strip)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
super(requirements)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Dependabot::Utils.register_requirement_class(
|
|
66
|
+
"apm",
|
|
67
|
+
Dependabot::Apm::Requirement
|
|
68
|
+
)
|