Commit e1009191 authored by Ethan Reesor's avatar Ethan Reesor

Refactor Go module proxy to add finders

parent 30232ef8
# frozen_string_literal: true
module Packages
module Go
class ModuleFinder
include ::API::Helpers::Packages::Go::ModuleHelpers
attr_reader :project, :module_name
def initialize(project, module_name)
module_name = Pathname.new(module_name).cleanpath.to_s
@project = project
@module_name = module_name
end
def execute
return if @module_name.blank?
if @module_name == package_base
Packages::GoModule.new(@project, @module_name, '')
elsif @module_name.start_with?(package_base + '/')
Packages::GoModule.new(@project, @module_name, @module_name[(package_base.length + 1)..])
else
nil
end
end
private
def package_base
@package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1]
end
end
end
end
# frozen_string_literal: true
module Packages
module Go
class VersionFinder
include ::API::Helpers::Packages::Go::ModuleHelpers
attr_reader :mod
def initialize(mod)
@mod = mod
end
def execute
@mod.project.repository.tags
.filter { |tag| semver? tag }
.map { |tag| find_ref tag }
.filter { |ver| ver.valid? }
end
def find(target)
case target
when String
unless pseudo_version? target
return mod.versions.filter { |v| v.name == target }.first
end
begin
find_pseudo_version target
rescue ArgumentError
nil
end
when Gitlab::Git::Ref
find_ref target
when ::Commit, Gitlab::Git::Commit
find_commit target
else
raise ArgumentError.new 'not a valid target'
end
end
private
def find_ref(ref)
commit = ref.dereferenced_target
Packages::GoModuleVersion.new(@mod, :ref, commit, ref: ref, semver: parse_semver(ref.name))
end
def find_commit(commit)
Packages::GoModuleVersion.new(@mod, :commit, commit)
end
def find_pseudo_version(str)
semver = parse_semver(str)
raise ArgumentError.new 'target is not a pseudo-version' unless semver && PSEUDO_VERSION_REGEX.match?(str)
# valid pseudo-versions are
# vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X
# vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre
# vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z
# go discards the timestamp when resolving pseudo-versions, so we will do the same
timestamp, sha = semver.prerelease.split('-').last 2
timestamp = timestamp.split('.').last
commit = @mod.project.repository.commit_by(oid: sha)
# these errors are copied from proxy.golang.org's responses
raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless commit
raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12
raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp
Packages::GoModuleVersion.new(@mod, :pseudo, commit, name: str, semver: semver)
end
end
end
end
...@@ -3,42 +3,32 @@ ...@@ -3,42 +3,32 @@
class Packages::GoModule class Packages::GoModule
attr_reader :project, :name, :path attr_reader :project, :name, :path
def initialize(project, name) def initialize(project, name, path)
@project = project @project = project
@name = name @name = name
@path = path
@path =
if @name == package_base
''
elsif @name.start_with?(package_base + '/')
@name[(package_base.length + 1)..]
else
nil
end
end end
def versions def versions
@versions ||= @project.repository.tags @versions ||= Packages::Go::VersionFinder.new(self).execute
.filter { |tag| ::Packages::GoModuleVersion.semver? tag }
.map { |tag| ::Packages::GoModuleVersion.new self, tag }
.filter { |ver| ver.valid? }
end end
def find_version(name) def find_version(name)
if ::Packages::GoModuleVersion.pseudo_version? name Packages::Go::VersionFinder.new(self).find(name)
begin
::Packages::GoModuleVersion.new self, name
rescue ArgumentError
nil
end end
def path_valid?(major)
m = /\/v(\d+)$/i.match(@name)
case major
when 0, 1
m.nil?
else else
versions.filter { |ver| ver.name == name }.first !m.nil? && m[1].to_i == major
end end
end end
private def gomod_valid?(gomod)
gomod&.split("\n", 2)&.first == "module #{@name}"
def package_base
@package_base ||= Gitlab::Routing.url_helpers.project_url(@project).split('://', 2)[1]
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class Packages::GoModuleVersion class Packages::GoModuleVersion
SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze include ::API::Helpers::Packages::Go::ModuleHelpers
SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze
PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze
VERSION_SUFFIX_REGEX = /\/v([1-9]\d*)$/i.freeze
attr_reader :mod, :type, :ref, :commit attr_reader :mod, :type, :ref, :commit
...@@ -14,57 +11,13 @@ class Packages::GoModuleVersion ...@@ -14,57 +11,13 @@ class Packages::GoModuleVersion
delegate :prerelease, to: :@semver, allow_nil: true delegate :prerelease, to: :@semver, allow_nil: true
delegate :build, to: :@semver, allow_nil: true delegate :build, to: :@semver, allow_nil: true
def self.semver?(tag) def initialize(mod, type, commit, name: nil, semver: nil, ref: nil)
return false if tag.dereferenced_target.nil?
SEMVER_TAG_REGEX.match?(tag.name)
end
def self.pseudo_version?(str)
SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str)
end
def initialize(mod, target)
@mod = mod @mod = mod
@type = type
case target @commit = commit
when String @name = name if name
m = SEMVER_TAG_REGEX.match(target) @semver = semver if semver
raise ArgumentError.new 'target is not a pseudo-version' unless m && PSEUDO_VERSION_REGEX.match?(target) @ref = ref if ref
# valid pseudo-versions are
# vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X
# vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre
# vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z
# go discards the timestamp when resolving pseudo-versions, so we will do the same
@type = :pseudo
@name = target
@semver = semver_match_to_hash m
timestamp, sha = prerelease.split('-').last 2
timestamp = timestamp.split('.').last
@commit = mod.project.repository.commit_by(oid: sha)
# these errors are copied from proxy.golang.org's responses
raise ArgumentError.new 'invalid pseudo-version: unknown commit' unless @commit
raise ArgumentError.new 'invalid pseudo-version: revision is shorter than canonical' unless sha.length == 12
raise ArgumentError.new 'invalid pseudo-version: does not match version-control timestamp' unless @commit.committed_date.strftime('%Y%m%d%H%M%S') == timestamp
when Gitlab::Git::Ref
@type = :ref
@ref = target
@commit = target.dereferenced_target
@semver = semver_match_to_hash SEMVER_TAG_REGEX.match(target.name)
when ::Commit, Gitlab::Git::Commit
@type = :commit
@commit = target
else
raise ArgumentError.new 'not a valid target'
end
end end
def name def name
...@@ -72,36 +25,11 @@ class Packages::GoModuleVersion ...@@ -72,36 +25,11 @@ class Packages::GoModuleVersion
end end
def gomod def gomod
@gomod ||= @mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data @gomod ||= blob_at(@mod.path + '/go.mod')
end
def valid?
valid_path? && valid_module?
end
def valid_path?
m = VERSION_SUFFIX_REGEX.match(@mod.name)
case major
when 0, 1
m.nil?
else
!m.nil? && m[1].to_i == major
end
end
def valid_module?
return false unless gomod
gomod.split("\n", 2).first == "module #{@mod.name}"
end
def pseudo?
@type == :pseudo
end end
def files def files
return @files unless @files.nil? return @files if defined?(@files)
sha = @commit.sha sha = @commit.sha
tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? } tree = @mod.project.repository.tree(sha, @mod.path, recursive: true).entries.filter { |e| e.file? }
...@@ -110,19 +38,10 @@ class Packages::GoModuleVersion ...@@ -110,19 +38,10 @@ class Packages::GoModuleVersion
end end
def blob_at(path) def blob_at(path)
@mod.project.repository.blob_at(@commit.sha, path).data @mod.project.repository.blob_at(@commit.sha, path)&.data
end end
private def valid?
@mod.path_valid?(major) && @mod.gomod_valid?(gomod)
def semver_match_to_hash(match)
return unless match
OpenStruct.new(
major: match[1].to_i,
minor: match[2].to_i,
patch: match[3].to_i,
prerelease: match[4],
build: match[5])
end end
end end
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
module API module API
class GoProxy < Grape::API class GoProxy < Grape::API
helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::Go::ModuleHelpers
# basic semver, except case encoded (A => !a)
MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze
MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze
before { require_packages_enabled! } before { require_packages_enabled! }
...@@ -15,25 +18,24 @@ module API ...@@ -15,25 +18,24 @@ module API
def find_module def find_module
module_name = case_decode params[:module_name] module_name = case_decode params[:module_name]
bad_request!('Module Name') if module_name.blank? bad_request!('Module Name') if module_name.blank?
mod = ::Packages::GoModule.new user_project, module_name mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute
not_found! if mod.path.nil?
not_found! unless mod
mod mod
end end
def find_version def find_version
mod = find_module module_version = case_decode params[:module_version]
ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version)
ver = mod.find_version case_decode params[:module_version]
not_found! unless ver&.valid? not_found! unless ver&.valid?
ver ver
end end
# override :find_project!
def find_project!(id) def find_project!(id)
project = find_project(id) project = find_project(id)
......
# frozen_string_literal: true
module API
module Helpers
module Packages
module Go
module ModuleHelpers
# basic semver regex
SEMVER_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?/i.freeze
# basic semver, but bounded (^expr$)
SEMVER_TAG_REGEX = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.a-z0-9]+))?(?:\+([-.a-z0-9]+))?$/i.freeze
# semver, but the prerelease component follows a specific format
PSEUDO_VERSION_REGEX = /^v\d+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/i.freeze
def case_encode(str)
str.gsub(/A-Z/) { |s| "!#{s.downcase}"}
end
def case_decode(str)
str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase }
end
def semver?(tag)
return false if tag.dereferenced_target.nil?
SEMVER_TAG_REGEX.match?(tag.name)
end
def pseudo_version?(str)
SEMVER_TAG_REGEX.match?(str) && PSEUDO_VERSION_REGEX.match?(str)
end
def parse_semver(str)
m = SEMVER_TAG_REGEX.match(str)
return unless m
OpenStruct.new(
major: m[1].to_i,
minor: m[2].to_i,
patch: m[3].to_i,
prerelease: m[4],
build: m[5])
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment