Commit 2dbb2234 authored by Sean McGivern's avatar Sean McGivern

Merge branch '285467-package-registry-graphql-api-3' into 'master'

Package: group and project graphql types - add search

See merge request gitlab-org/gitlab!61001
parents 0df2955c a30c143b
# frozen_string_literal: true # frozen_string_literal: true
# rubocop: disable Graphql/ResolverType
module Resolvers module Resolvers
class GroupPackagesResolver < BaseResolver class GroupPackagesResolver < PackagesBaseResolver
type Types::Packages::PackageType.connection_type, null: true # The GraphQL type is defined in the extended class
argument :sort, Types::Packages::PackageGroupSortEnum, argument :sort, Types::Packages::PackageGroupSortEnum,
description: 'Sort packages by this criteria.', description: 'Sort packages by this criteria.',
required: false, required: false,
default_value: :created_desc default_value: :created_desc
SORT_TO_PARAMS_MAP = { GROUP_SORT_TO_PARAMS_MAP = SORT_TO_PARAMS_MAP.merge({
created_desc: { order_by: 'created', sort: 'desc' },
created_asc: { order_by: 'created', sort: 'asc' },
name_desc: { order_by: 'name', sort: 'desc' },
name_asc: { order_by: 'name', sort: 'asc' },
version_desc: { order_by: 'version', sort: 'desc' },
version_asc: { order_by: 'version', sort: 'asc' },
type_desc: { order_by: 'type', sort: 'desc' },
type_asc: { order_by: 'type', sort: 'asc' },
project_path_desc: { order_by: 'project_path', sort: 'desc' }, project_path_desc: { order_by: 'project_path', sort: 'desc' },
project_path_asc: { order_by: 'project_path', sort: 'asc' } project_path_asc: { order_by: 'project_path', sort: 'asc' }
}.freeze }).freeze
def ready?(**args) def ready?(**args)
context[self.class] ||= { executions: 0 } context[self.class] ||= { executions: 0 }
...@@ -30,16 +23,11 @@ module Resolvers ...@@ -30,16 +23,11 @@ module Resolvers
super super
end end
def resolve(sort:) def resolve(sort:, **filters)
return unless packages_available? return unless packages_available?
::Packages::GroupPackagesFinder.new(current_user, object, SORT_TO_PARAMS_MAP[sort]).execute ::Packages::GroupPackagesFinder.new(current_user, object, filters.merge(GROUP_SORT_TO_PARAMS_MAP.fetch(sort))).execute
end
private
def packages_available?
::Gitlab.config.packages.enabled
end end
end end
end end
# rubocop: enable Graphql/ResolverType
# frozen_string_literal: true
module Resolvers
class PackagesBaseResolver < BaseResolver
type Types::Packages::PackageType.connection_type, null: true
argument :sort, Types::Packages::PackageSortEnum,
description: 'Sort packages by this criteria.',
required: false,
default_value: :created_desc
argument :package_name, GraphQL::STRING_TYPE,
description: 'Search a package by name.',
required: false,
default_value: nil
argument :package_type, Types::Packages::PackageTypeEnum,
description: 'Filter a package by type.',
required: false,
default_value: nil
argument :status, Types::Packages::PackageStatusEnum,
description: 'Filter a package by status.',
required: false,
default_value: nil
argument :include_versionless, GraphQL::BOOLEAN_TYPE,
description: 'Include versionless packages.',
required: false,
default_value: false
SORT_TO_PARAMS_MAP = {
created_desc: { order_by: 'created', sort: 'desc' },
created_asc: { order_by: 'created', sort: 'asc' },
name_desc: { order_by: 'name', sort: 'desc' },
name_asc: { order_by: 'name', sort: 'asc' },
version_desc: { order_by: 'version', sort: 'desc' },
version_asc: { order_by: 'version', sort: 'asc' },
type_desc: { order_by: 'type', sort: 'desc' },
type_asc: { order_by: 'type', sort: 'asc' }
}.freeze
def resolve
raise NotImplementedError
end
private
def packages_available?
::Gitlab.config.packages.enabled
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
# rubocop: disable Graphql/ResolverType
module Resolvers module Resolvers
class ProjectPackagesResolver < BaseResolver class ProjectPackagesResolver < PackagesBaseResolver
type Types::Packages::PackageType.connection_type, null: true # The GraphQL type is defined in the extended class
argument :sort, Types::Packages::PackageSortEnum, def resolve(sort:, **filters)
description: 'Sort packages by this criteria.',
required: false,
default_value: :created_desc
SORT_TO_PARAMS_MAP = {
created_desc: { order_by: 'created', sort: 'desc' },
created_asc: { order_by: 'created', sort: 'asc' },
name_desc: { order_by: 'name', sort: 'desc' },
name_asc: { order_by: 'name', sort: 'asc' },
version_desc: { order_by: 'version', sort: 'desc' },
version_asc: { order_by: 'version', sort: 'asc' },
type_desc: { order_by: 'type', sort: 'desc' },
type_asc: { order_by: 'type', sort: 'asc' }
}.freeze
def resolve(sort:)
return unless packages_available? return unless packages_available?
::Packages::PackagesFinder.new(object, SORT_TO_PARAMS_MAP.fetch(sort)).execute ::Packages::PackagesFinder.new(object, filters.merge(SORT_TO_PARAMS_MAP.fetch(sort))).execute
end
private
def packages_available?
::Gitlab.config.packages.enabled
end end
end end
end end
# rubocop: enable Graphql/ResolverType
---
title: 'Package: group and project graphql types - add search'
merge_request: 61001
author:
type: added
...@@ -9107,7 +9107,11 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -9107,7 +9107,11 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="grouppackagesincludeversionless"></a>`includeVersionless` | [`Boolean`](#boolean) | Include versionless packages. |
| <a id="grouppackagespackagename"></a>`packageName` | [`String`](#string) | Search a package by name. |
| <a id="grouppackagespackagetype"></a>`packageType` | [`PackageTypeEnum`](#packagetypeenum) | Filter a package by type. |
| <a id="grouppackagessort"></a>`sort` | [`PackageGroupSort`](#packagegroupsort) | Sort packages by this criteria. | | <a id="grouppackagessort"></a>`sort` | [`PackageGroupSort`](#packagegroupsort) | Sort packages by this criteria. |
| <a id="grouppackagesstatus"></a>`status` | [`PackageStatus`](#packagestatus) | Filter a package by status. |
##### `Group.projects` ##### `Group.projects`
...@@ -11362,7 +11366,11 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -11362,7 +11366,11 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="projectpackagesincludeversionless"></a>`includeVersionless` | [`Boolean`](#boolean) | Include versionless packages. |
| <a id="projectpackagespackagename"></a>`packageName` | [`String`](#string) | Search a package by name. |
| <a id="projectpackagespackagetype"></a>`packageType` | [`PackageTypeEnum`](#packagetypeenum) | Filter a package by type. |
| <a id="projectpackagessort"></a>`sort` | [`PackageSort`](#packagesort) | Sort packages by this criteria. | | <a id="projectpackagessort"></a>`sort` | [`PackageSort`](#packagesort) | Sort packages by this criteria. |
| <a id="projectpackagesstatus"></a>`status` | [`PackageStatus`](#packagestatus) | Filter a package by status. |
##### `Project.pipeline` ##### `Project.pipeline`
......
...@@ -16,38 +16,6 @@ RSpec.describe Resolvers::GroupPackagesResolver do ...@@ -16,38 +16,6 @@ RSpec.describe Resolvers::GroupPackagesResolver do
describe '#resolve' do describe '#resolve' do
subject { resolve(described_class, ctx: { current_user: user }, obj: group, args: args).to_a } subject { resolve(described_class, ctx: { current_user: user }, obj: group, args: args).to_a }
context 'without sort' do it_behaves_like 'group and projects packages resolver'
let_it_be(:package) { create(:package, project: project) }
it { is_expected.to contain_exactly(package) }
end
context 'with a sort argument' do
let_it_be(:project2) { create(:project, :public, group: group) }
let_it_be(:sort_repository) do
create(:conan_package, name: 'bar', project: project, created_at: 1.day.ago, version: "1.0.0")
end
let_it_be(:sort_repository2) do
create(:maven_package, name: 'foo', project: project2, created_at: 1.hour.ago, version: "2.0.0")
end
[:created_desc, :name_desc, :version_desc, :type_asc, :project_path_desc].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
it { is_expected.to eq([sort_repository2, sort_repository]) }
end
end
[:created_asc, :name_asc, :version_asc, :type_desc, :project_path_asc].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
it { is_expected.to eq([sort_repository, sort_repository2]) }
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::PackagesBaseResolver do
include GraphqlHelpers
describe '#resolve' do
subject { resolve(described_class) }
it 'throws an error' do
expect { subject }.to raise_error(NotImplementedError)
end
end
end
...@@ -6,7 +6,7 @@ RSpec.describe Resolvers::ProjectPackagesResolver do ...@@ -6,7 +6,7 @@ RSpec.describe Resolvers::ProjectPackagesResolver do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) } let_it_be_with_reload(:project) { create(:project, :public) }
let(:args) do let(:args) do
{ sort: :created_desc } { sort: :created_desc }
...@@ -15,36 +15,6 @@ RSpec.describe Resolvers::ProjectPackagesResolver do ...@@ -15,36 +15,6 @@ RSpec.describe Resolvers::ProjectPackagesResolver do
describe '#resolve' do describe '#resolve' do
subject { resolve(described_class, ctx: { current_user: user }, obj: project, args: args).to_a } subject { resolve(described_class, ctx: { current_user: user }, obj: project, args: args).to_a }
context 'without sort' do it_behaves_like 'group and projects packages resolver'
let_it_be(:package) { create(:package, project: project) }
it { is_expected.to contain_exactly(package) }
end
context 'with a sort argument' do
let_it_be(:sort_repository) do
create(:conan_package, name: 'bar', project: project, created_at: 1.day.ago, version: "1.0.0")
end
let_it_be(:sort_repository2) do
create(:maven_package, name: 'foo', project: project, created_at: 1.hour.ago, version: "2.0.0")
end
[:created_desc, :name_desc, :version_desc, :type_asc].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
it { is_expected.to eq([sort_repository2, sort_repository]) }
end
end
[:created_asc, :name_asc, :version_asc, :type_desc].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
it { is_expected.to eq([sort_repository, sort_repository2]) }
end
end
end
end end
end end
...@@ -7,82 +7,19 @@ RSpec.describe 'getting a package list for a group' do ...@@ -7,82 +7,19 @@ RSpec.describe 'getting a package list for a group' do
let_it_be(:resource) { create(:group, :private) } let_it_be(:resource) { create(:group, :private) }
let_it_be(:group_two) { create(:group, :private) } let_it_be(:group_two) { create(:group, :private) }
let_it_be(:project) { create(:project, :repository, group: resource) } let_it_be(:project1) { create(:project, :repository, group: resource) }
let_it_be(:another_project) { create(:project, :repository, group: resource) } let_it_be(:project2) { create(:project, :repository, group: resource) }
let_it_be(:group_two_project) { create(:project, :repository, group: group_two) }
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:npm_package) { create(:npm_package, project: group_two_project) } let(:resource_type) { :group }
let_it_be(:maven_package) { create(:maven_package, project: project, name: 'tab', version: '4.0.0', created_at: 5.days.ago) }
let_it_be(:package) { create(:npm_package, project: project, name: 'uab', version: '5.0.0', created_at: 4.days.ago) }
let_it_be(:composer_package) { create(:composer_package, project: another_project, name: 'vab', version: '6.0.0', created_at: 3.days.ago) }
let_it_be(:debian_package) { create(:debian_package, project: another_project, name: 'zab', version: '7.0.0', created_at: 2.days.ago) }
let_it_be(:composer_metadatum) do
create(:composer_metadatum, package: composer_package,
target_sha: 'afdeh',
composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
end
let(:package_names) { graphql_data_at(:group, :packages, :nodes, :name) }
let(:target_shas) { graphql_data_at(:group, :packages, :nodes, :metadata, :target_sha) }
let(:packages) { graphql_data_at(:group, :packages, :nodes) }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
metadata { #{query_graphql_fragment('ComposerMetadata')} }
}
QUERY
end
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => resource.full_path },
query_graphql_field('packages', {}, fields)
)
end
it_behaves_like 'group and project packages query' it_behaves_like 'group and project packages query'
describe 'sorting and pagination' do
let_it_be(:ascending_packages) { [maven_package, package, composer_package, debian_package].map { |package| global_id_of(package)} }
let(:data_path) { [:group, :packages] }
before do
resource.add_reporter(current_user)
end
[:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC].each do |order|
context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
let(:expected_results) { ascending_packages }
end
end
end
[:CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order|
context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
let(:expected_results) { ascending_packages.reverse }
end
end
end
def pagination_query(params)
graphql_query_for(:group, { 'fullPath' => resource.full_path },
query_nodes(:packages, :id, include_pagination_info: true, args: params)
)
end
end
context 'with a batched query' do context 'with a batched query' do
let_it_be(:group_two_project) { create(:project, :repository, group: group_two) }
let_it_be(:group_one_package) { create(:npm_package, project: project1) }
let_it_be(:group_two_package) { create(:npm_package, project: group_two_project) }
let(:batch_query) do let(:batch_query) do
<<~QUERY <<~QUERY
{ {
...@@ -101,12 +38,7 @@ RSpec.describe 'getting a package list for a group' do ...@@ -101,12 +38,7 @@ RSpec.describe 'getting a package list for a group' do
end end
it 'returns an error for the second group and data for the first' do it 'returns an error for the second group and data for the first' do
expect(a_packages_names).to contain_exactly( expect(a_packages_names).to contain_exactly(group_one_package.name)
package.name,
maven_package.name,
debian_package.name,
composer_package.name
)
expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/] expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/]
expect(graphql_data_at(:b, :packages)).to be(nil) expect(graphql_data_at(:b, :packages)).to be(nil)
end end
......
...@@ -7,72 +7,10 @@ RSpec.describe 'getting a package list for a project' do ...@@ -7,72 +7,10 @@ RSpec.describe 'getting a package list for a project' do
let_it_be(:resource) { create(:project, :repository) } let_it_be(:resource) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:maven_package) { create(:maven_package, project: resource, name: 'tab', version: '4.0.0', created_at: 5.days.ago) } let_it_be(:project1) { resource }
let_it_be(:package) { create(:npm_package, project: resource, name: 'uab', version: '5.0.0', created_at: 4.days.ago) } let_it_be(:project2) { resource }
let_it_be(:composer_package) { create(:composer_package, project: resource, name: 'vab', version: '6.0.0', created_at: 3.days.ago) }
let_it_be(:debian_package) { create(:debian_package, project: resource, name: 'zab', version: '7.0.0', created_at: 2.days.ago) }
let_it_be(:composer_metadatum) do
create(:composer_metadatum, package: composer_package,
target_sha: 'afdeh',
composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
end
let(:package_names) { graphql_data_at(:project, :packages, :nodes, :name) } let(:resource_type) { :project }
let(:target_shas) { graphql_data_at(:project, :packages, :nodes, :metadata, :target_sha) }
let(:packages) { graphql_data_at(:project, :packages, :nodes) }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
metadata { #{query_graphql_fragment('ComposerMetadata')} }
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => resource.full_path },
query_graphql_field('packages', {}, fields)
)
end
it_behaves_like 'group and project packages query' it_behaves_like 'group and project packages query'
describe 'sorting and pagination' do
let_it_be(:ascending_packages) { [maven_package, package, composer_package, debian_package].map { |package| global_id_of(package)} }
let(:data_path) { [:project, :packages] }
before do
resource.add_reporter(current_user)
end
[:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC].each do |order|
context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
let(:expected_results) { ascending_packages }
end
end
end
[:CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order|
context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
let(:expected_results) { ascending_packages.reverse }
end
end
end
def pagination_query(params)
graphql_query_for(:project, { 'fullPath' => resource.full_path },
query_nodes(:packages, :id, include_pagination_info: true, args: params)
)
end
end
end end
# frozen_string_literal: true
RSpec.shared_examples 'group and projects packages resolver' do
context 'without sort' do
let_it_be(:npm_package) { create(:package, project: project) }
it { is_expected.to contain_exactly(npm_package) }
end
context 'with sorting and filtering' do
let_it_be(:conan_package) do
create(:conan_package, name: 'bar', project: project, created_at: 1.day.ago, version: "1.0.0", status: 'default')
end
let_it_be(:maven_package) do
create(:maven_package, name: 'foo', project: project, created_at: 1.hour.ago, version: "2.0.0", status: 'error')
end
let_it_be(:repository3) do
create(:maven_package, name: 'baz', project: project, created_at: 1.minute.ago, version: nil)
end
[:created_desc, :name_desc, :version_desc, :type_asc].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
it { is_expected.to eq([maven_package, conan_package]) }
end
end
[:created_asc, :name_asc, :version_asc, :type_desc].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
it { is_expected.to eq([conan_package, maven_package]) }
end
end
context 'filter by package_name' do
let(:args) { { package_name: 'bar', sort: :created_desc } }
it { is_expected.to eq([conan_package]) }
end
context 'filter by package_type' do
let(:args) { { package_type: 'conan', sort: :created_desc } }
it { is_expected.to eq([conan_package]) }
end
context 'filter by status' do
let(:args) { { status: 'error', sort: :created_desc } }
it { is_expected.to eq([maven_package]) }
end
context 'include_versionless' do
let(:args) { { include_versionless: true, sort: :created_desc } }
it { is_expected.to include(repository3) }
end
end
end
...@@ -3,6 +3,38 @@ ...@@ -3,6 +3,38 @@
RSpec.shared_examples 'group and project packages query' do RSpec.shared_examples 'group and project packages query' do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:versionaless_package) { create(:maven_package, project: project1, version: nil) }
let_it_be(:maven_package) { create(:maven_package, project: project1, name: 'tab', version: '4.0.0', created_at: 5.days.ago) }
let_it_be(:package) { create(:npm_package, project: project1, name: 'uab', version: '5.0.0', created_at: 4.days.ago) }
let_it_be(:composer_package) { create(:composer_package, project: project2, name: 'vab', version: '6.0.0', created_at: 3.days.ago) }
let_it_be(:debian_package) { create(:debian_package, project: project2, name: 'zab', version: '7.0.0', created_at: 2.days.ago) }
let_it_be(:composer_metadatum) do
create(:composer_metadatum, package: composer_package,
target_sha: 'afdeh',
composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
end
let(:package_names) { graphql_data_at(resource_type, :packages, :nodes, :name) }
let(:target_shas) { graphql_data_at(resource_type, :packages, :nodes, :metadata, :target_sha) }
let(:packages) { graphql_data_at(resource_type, :packages, :nodes) }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
metadata { #{query_graphql_fragment('ComposerMetadata')} }
}
QUERY
end
let(:query) do
graphql_query_for(
resource_type,
{ 'fullPath' => resource.full_path },
query_graphql_field('packages', {}, fields)
)
end
context 'when user has access to the resource' do context 'when user has access to the resource' do
before do before do
resource.add_reporter(current_user) resource.add_reporter(current_user)
...@@ -48,4 +80,101 @@ RSpec.shared_examples 'group and project packages query' do ...@@ -48,4 +80,101 @@ RSpec.shared_examples 'group and project packages query' do
expect(packages).to be_nil expect(packages).to be_nil
end end
end end
describe 'sorting and pagination' do
let_it_be(:ascending_packages) { [maven_package, package, composer_package, debian_package].map { |package| global_id_of(package)} }
let(:data_path) { [resource_type, :packages] }
before do
resource.add_reporter(current_user)
end
[:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC].each do |order|
context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
let(:expected_results) { ascending_packages }
end
end
end
[:CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order|
context "#{order}" do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { order }
let(:first_param) { 4 }
let(:expected_results) { ascending_packages.reverse }
end
end
end
context 'with an invalid sort' do
let(:query) do
graphql_query_for(
resource_type,
{ 'fullPath' => resource.full_path },
query_nodes(:packages, :name, args: { sort: :WRONG_ORDER })
)
end
before do
post_graphql(query, current_user: current_user)
end
it 'throws an error' do
expect_graphql_errors_to_include(/Argument \'sort\' on Field \'packages\' has an invalid value/)
end
end
def pagination_query(params)
graphql_query_for(resource_type, { 'fullPath' => resource.full_path },
query_nodes(:packages, :id, include_pagination_info: true, args: params)
)
end
end
describe 'filtering' do
subject { packages }
let(:query) do
graphql_query_for(
resource_type,
{ 'fullPath' => resource.full_path },
query_nodes(:packages, :name, args: params)
)
end
before do
resource.add_reporter(current_user)
post_graphql(query, current_user: current_user)
end
context 'package_name' do
let(:params) { { package_name: maven_package.name } }
it { is_expected.to contain_exactly({ "name" => maven_package.name }) }
end
context 'package_type' do
let(:params) { { package_type: :COMPOSER } }
it { is_expected.to contain_exactly({ "name" => composer_package.name }) }
end
context 'status' do
let_it_be(:errored_package) { create(:maven_package, project: project1, status: 'error') }
let(:params) { { status: :ERROR } }
it { is_expected.to contain_exactly({ "name" => errored_package.name }) }
end
context 'include_versionless' do
let(:params) { { include_versionless: true } }
it { is_expected.to include({ "name" => versionaless_package.name }) }
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