Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-17 12:09:26 +00:00
parent 839e879bcf
commit 75a4eaade0
50 changed files with 343 additions and 363 deletions

View File

@ -522,7 +522,7 @@ export default {
:file="file" :file="file"
:reviewed="fileReviews[index]" :reviewed="fileReviews[index]"
:is-first-file="index === 0" :is-first-file="index === 0"
:is-last-file="index === diffs.length - 1" :is-last-file="index === diffFilesLength - 1"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork" :can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile" :view-diffs-file-by-file="viewDiffsFileByFile"

View File

@ -57,7 +57,10 @@ export default {
<template> <template>
<div> <div>
<label class="dropdown-input pt-3 pb-3 mb-0 border-bottom block position-relative" @click.stop> <label
class="dropdown-input gl-pt-3 gl-pb-5 gl-mb-0 gl-border-b-1 gl-border-b-solid gl-display-block"
@click.stop
>
<input <input
ref="searchInput" ref="searchInput"
v-model="search" v-model="search"

View File

@ -75,7 +75,10 @@ export default {
<template> <template>
<div> <div>
<label class="dropdown-input pt-3 pb-3 mb-0 border-bottom block" @click.stop> <label
class="dropdown-input gl-pt-3 gl-pb-5 gl-mb-0 gl-border-b-1 gl-border-b-solid gl-display-block"
@click.stop
>
<tokened-input <tokened-input
v-model="search" v-model="search"
:tokens="searchTokens" :tokens="searchTokens"

View File

@ -1,13 +1,12 @@
<script> <script>
import Tab from '~/vue_shared/components/tabs/tab.vue'; import { GlTab, GlTabs } from '@gitlab/ui';
import Tabs from '~/vue_shared/components/tabs/tabs';
import BranchesSearchList from './branches/search_list.vue'; import BranchesSearchList from './branches/search_list.vue';
import MergeRequestSearchList from './merge_requests/list.vue'; import MergeRequestSearchList from './merge_requests/list.vue';
export default { export default {
components: { components: {
Tabs, GlTab,
Tab, GlTabs,
BranchesSearchList, BranchesSearchList,
MergeRequestSearchList, MergeRequestSearchList,
}, },
@ -23,20 +22,14 @@ export default {
<template> <template>
<div class="ide-nav-form p-0"> <div class="ide-nav-form p-0">
<tabs v-if="showMergeRequests" stop-propagation> <gl-tabs v-if="showMergeRequests">
<tab active> <gl-tab :title="__('Branches')">
<template #title>
{{ __('Branches') }}
</template>
<branches-search-list /> <branches-search-list />
</tab> </gl-tab>
<tab> <gl-tab :title="__('Merge Requests')">
<template #title>
{{ __('Merge Requests') }}
</template>
<merge-request-search-list /> <merge-request-search-list />
</tab> </gl-tab>
</tabs> </gl-tabs>
<branches-search-list v-else /> <branches-search-list v-else />
</div> </div>
</template> </template>

View File

@ -1,47 +0,0 @@
<script>
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
active: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
// props can't be updated, so we map it to data where we can
localActive: this.active,
};
},
watch: {
active() {
this.localActive = this.active;
},
},
created() {
this.isTab = true;
},
updated() {
if (this.$parent) {
this.$parent.$forceUpdate();
}
},
};
</script>
<template>
<div
:class="{
active: localActive,
}"
class="tab-pane"
role="tabpanel"
>
<slot></slot>
</div>
</template>

View File

@ -1,76 +0,0 @@
export default {
props: {
stopPropagation: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
currentIndex: 0,
tabs: [],
};
},
mounted() {
this.updateTabs();
},
methods: {
updateTabs() {
this.tabs = this.$children.filter((child) => child.isTab);
this.currentIndex = this.tabs.findIndex((tab) => tab.localActive);
},
setTab(e, index) {
if (this.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
this.tabs[this.currentIndex].localActive = false;
this.tabs[index].localActive = true;
this.currentIndex = index;
},
},
render(h) {
const navItems = this.tabs.map((tab, i) =>
h(
'li',
{
key: i,
},
[
h(
'a',
{
class: tab.localActive ? 'active' : null,
attrs: {
href: '#',
},
on: {
click: (e) => this.setTab(e, i),
},
},
tab.$slots.title || tab.title,
),
],
),
);
const nav = h(
'ul',
{
class: 'nav-links tab-links',
},
[navItems],
);
const content = h(
'div',
{
class: ['tab-content'],
},
[this.$slots.default],
);
return h('div', {}, [[nav], content]);
},
};

View File

@ -1043,8 +1043,7 @@ $ide-commit-header-height: 48px;
.input-icon { .input-icon {
right: auto; right: auto;
left: 10px; left: 10px;
top: 50%; top: 1rem;
transform: translateY(-50%);
} }
} }

View File

@ -4,6 +4,9 @@ module Packages
module FinderHelper module FinderHelper
extend ActiveSupport::Concern extend ActiveSupport::Concern
InvalidPackageTypeError = Class.new(StandardError)
InvalidStatusError = Class.new(StandardError)
private private
def packages_visible_to_user(user, within_group:) def packages_visible_to_user(user, within_group:)
@ -25,5 +28,35 @@ module Packages
::Project.in_namespace(namespace_ids) ::Project.in_namespace(namespace_ids)
.public_or_visible_to_user(user, ::Gitlab::Access::REPORTER) .public_or_visible_to_user(user, ::Gitlab::Access::REPORTER)
end end
def package_type
params[:package_type].presence
end
def filter_by_package_type(packages)
return packages unless package_type
raise InvalidPackageTypeError unless ::Packages::Package.package_types.key?(package_type)
packages.with_package_type(package_type)
end
def filter_by_package_name(packages)
return packages unless params[:package_name].present?
packages.search_by_name(params[:package_name])
end
def filter_with_version(packages)
return packages if params[:include_versionless].present?
packages.has_version
end
def filter_by_status(packages)
return packages.displayable unless params[:status].present?
raise InvalidStatusError unless Package.statuses.key?(params[:status])
packages.with_status(params[:status])
end
end end
end end

View File

@ -2,9 +2,7 @@
module Packages module Packages
class GroupPackagesFinder class GroupPackagesFinder
attr_reader :current_user, :group, :params include ::Packages::FinderHelper
InvalidPackageTypeError = Class.new(StandardError)
def initialize(current_user, group, params = { exclude_subgroups: false, order_by: 'created_at', sort: 'asc' }) def initialize(current_user, group, params = { exclude_subgroups: false, order_by: 'created_at', sort: 'asc' })
@current_user = current_user @current_user = current_user
@ -20,6 +18,8 @@ module Packages
private private
attr_reader :current_user, :group, :params
def packages_for_group_projects def packages_for_group_projects
packages = ::Packages::Package packages = ::Packages::Package
.including_build_info .including_build_info
@ -32,6 +32,7 @@ module Packages
packages = filter_with_version(packages) packages = filter_with_version(packages)
packages = filter_by_package_type(packages) packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages) packages = filter_by_package_name(packages)
packages = filter_by_status(packages)
packages packages
end end
@ -46,10 +47,6 @@ module Packages
.with_feature_available_for_user(:repository, current_user) .with_feature_available_for_user(:repository, current_user)
end end
def package_type
params[:package_type].presence
end
def groups def groups
return [group] if exclude_subgroups? return [group] if exclude_subgroups?
@ -59,24 +56,5 @@ module Packages
def exclude_subgroups? def exclude_subgroups?
params[:exclude_subgroups] params[:exclude_subgroups]
end end
def filter_by_package_type(packages)
return packages unless package_type
raise InvalidPackageTypeError unless Package.package_types.key?(package_type)
packages.with_package_type(package_type)
end
def filter_by_package_name(packages)
return packages unless params[:package_name].present?
packages.search_by_name(params[:package_name])
end
def filter_with_version(packages)
return packages if params[:include_versionless].present?
packages.has_version
end
end end
end end

View File

@ -2,7 +2,7 @@
module Packages module Packages
class PackagesFinder class PackagesFinder
attr_reader :params, :project include ::Packages::FinderHelper
def initialize(project, params = {}) def initialize(project, params = {})
@project = project @project = project
@ -21,29 +21,14 @@ module Packages
packages = filter_with_version(packages) packages = filter_with_version(packages)
packages = filter_by_package_type(packages) packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages) packages = filter_by_package_name(packages)
packages = filter_by_status(packages)
packages = order_packages(packages) packages = order_packages(packages)
packages packages
end end
private private
def filter_with_version(packages) attr_reader :params, :project
return packages if params[:include_versionless].present?
packages.has_version
end
def filter_by_package_type(packages)
return packages unless params[:package_type]
packages.with_package_type(params[:package_type])
end
def filter_by_package_name(packages)
return packages unless params[:package_name]
packages.search_by_name(params[:package_name])
end
def order_packages(packages) def order_packages(packages)
packages.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}") packages.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}")

View File

@ -69,6 +69,8 @@ class Packages::Package < ApplicationRecord
composer: 6, generic: 7, golang: 8, debian: 9, composer: 6, generic: 7, golang: 8, debian: 9,
rubygems: 10 } rubygems: 10 }
enum status: { default: 0, hidden: 1, processing: 2 }
scope :with_name, ->(name) { where(name: name) } scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) } scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
scope :with_normalized_pypi_name, ->(name) { where("LOWER(regexp_replace(name, '[-_.]+', '-', 'g')) = ?", name.downcase) } scope :with_normalized_pypi_name, ->(name) { where("LOWER(regexp_replace(name, '[-_.]+', '-', 'g')) = ?", name.downcase) }
@ -76,6 +78,8 @@ class Packages::Package < ApplicationRecord
scope :with_version, ->(version) { where(version: version) } scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) } scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) } scope :with_package_type, ->(package_type) { where(package_type: package_type) }
scope :with_status, ->(status) { where(status: status) }
scope :displayable, -> { with_status(:default) }
scope :including_build_info, -> { includes(pipelines: :user) } scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) } scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) } scope :including_tags, -> { includes(:tags) }

View File

@ -9,7 +9,9 @@ module Packages
.packages .packages
.with_package_type(package_type) .with_package_type(package_type)
.safe_find_or_create_by!(name: name, version: version) do |package| .safe_find_or_create_by!(name: name, version: version) do |package|
package.status = params[:status] if params[:status]
package.creator = package_creator package.creator = package_creator
add_build_info(package) add_build_info(package)
end end
end end
@ -29,8 +31,9 @@ module Packages
{ {
creator: package_creator, creator: package_creator,
name: params[:name], name: params[:name],
version: params[:version] version: params[:version],
}.merge(attrs) status: params[:status]
}.compact.merge(attrs)
end end
def package_creator def package_creator

View File

@ -15,13 +15,16 @@ module Packages
package_params = { package_params = {
name: params[:package_name], name: params[:package_name],
version: params[:package_version], version: params[:package_version],
build: params[:build] build: params[:build],
status: params[:status]
} }
package = ::Packages::Generic::FindOrCreatePackageService package = ::Packages::Generic::FindOrCreatePackageService
.new(project, current_user, package_params) .new(project, current_user, package_params)
.execute .execute
package.update_column(:status, params[:status]) if params[:status] && params[:status] != package.status
package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present? package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present?
package package
end end

View File

@ -42,6 +42,7 @@ module Packages
package_params = { package_params = {
name: package_name, name: package_name,
path: params[:path], path: params[:path],
status: params[:status],
version: version version: version
} }

View File

@ -0,0 +1,5 @@
---
title: Add status attribute to packages and ability to set 'hidden' for generic packages
merge_request: 53385
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Convert IDE nav form tab to GlTab
merge_request: 54274
author:
type: changed

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddStatusToPackagesPackages < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :packages_packages, :status, :smallint, default: 0, null: false
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddStatusIndexToPackagesPackages < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_packages_packages_on_project_id_and_status'
def up
add_concurrent_index :packages_packages, [:project_id, :status], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :packages_packages, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
cb9f4b4e627cbc163653fc01c0542ef0ce87139b376c55bbaa4d7ab23e9b8973

View File

@ -0,0 +1 @@
5c661c453922181b350b8551d9a8f9b097e568459a2c2d128e41d9aefb026ab5

View File

@ -15244,7 +15244,8 @@ CREATE TABLE packages_packages (
name character varying NOT NULL, name character varying NOT NULL,
version character varying, version character varying,
package_type smallint NOT NULL, package_type smallint NOT NULL,
creator_id integer creator_id integer,
status smallint DEFAULT 0 NOT NULL
); );
CREATE SEQUENCE packages_packages_id_seq CREATE SEQUENCE packages_packages_id_seq
@ -22886,6 +22887,8 @@ CREATE INDEX index_packages_packages_on_project_id_and_created_at ON packages_pa
CREATE INDEX index_packages_packages_on_project_id_and_package_type ON packages_packages USING btree (project_id, package_type); CREATE INDEX index_packages_packages_on_project_id_and_package_type ON packages_packages USING btree (project_id, package_type);
CREATE INDEX index_packages_packages_on_project_id_and_status ON packages_packages USING btree (project_id, status);
CREATE INDEX index_packages_packages_on_project_id_and_version ON packages_packages USING btree (project_id, version); CREATE INDEX index_packages_packages_on_project_id_and_version ON packages_packages USING btree (project_id, version);
CREATE INDEX index_packages_project_id_name_partial_for_nuget ON packages_packages USING btree (project_id, name) WHERE (((name)::text <> 'NuGet.Temporary.Package'::text) AND (version IS NOT NULL) AND (package_type = 4)); CREATE INDEX index_packages_project_id_name_partial_for_nuget ON packages_packages USING btree (project_id, name) WHERE (((name)::text <> 'NuGet.Temporary.Package'::text) AND (version IS NOT NULL) AND (package_type = 4));

View File

@ -29,6 +29,7 @@ GET /projects/:id/packages
| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, or `golang`. (_Introduced in GitLab 12.9_) | `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, or `golang`. (_Introduced in GitLab 12.9_)
| `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_Introduced in GitLab 12.9_) | `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_Introduced in GitLab 12.9_)
| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_) | `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_)
| `status` | string | no | Filter the returned packages by status. One of `default` (default), `hidden`, or `processing`. (_Introduced in GitLab 13.9_)
```shell ```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages"
@ -70,6 +71,9 @@ Example response:
By default, the `GET` request returns 20 results, because the API is [paginated](README.md#pagination). By default, the `GET` request returns 20 results, because the API is [paginated](README.md#pagination).
Although you can filter packages by status, working with packages that have a `processing` status
can result in malformed data or broken packages.
### Within a group ### Within a group
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18871) in GitLab 12.5. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18871) in GitLab 12.5.
@ -90,6 +94,7 @@ GET /groups/:id/packages
| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, or `golang`. (_Introduced in GitLab 12.9_) | | `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, or `golang`. (_Introduced in GitLab 12.9_) |
| `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30980) in GitLab 13.0_) | `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30980) in GitLab 13.0_)
| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_) | `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_)
| `status` | string | no | Filter the returned packages by status. One of `default` (default), `hidden`, or `processing`. (_Introduced in GitLab 13.9_)
```shell ```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=false" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=false"
@ -166,6 +171,9 @@ The `_links` object contains the following properties:
- `web_path`: The path which you can visit in GitLab and see the details of the package. - `web_path`: The path which you can visit in GitLab and see the details of the package.
- `delete_api_path`: The API path to delete the package. Only available if the request user has permission to do so. - `delete_api_path`: The API path to delete the package. Only available if the request user has permission to do so.
Although you can filter packages by status, working with packages that have a `processing` status
can result in malformed data or broken packages.
## Get a project package ## Get a project package
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9667) in GitLab 11.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9667) in GitLab 11.9.

View File

@ -41,7 +41,7 @@ toggle the list of the milestone bars.
> - Filtering roadmaps by milestone is enabled on GitLab.com. > - Filtering roadmaps by milestone is enabled on GitLab.com.
> - Filtering roadmaps by milestone is recommended for production use. > - Filtering roadmaps by milestone is recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-filtering-roadmaps-by-milestone). **(PREMIUM SELF)** > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-filtering-roadmaps-by-milestone). **(PREMIUM SELF)**
> - Filtering by epic confidentiality [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218624) in GitLab 13.8. > - Filtering by epic confidentiality [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218624) in GitLab 13.9.
WARNING: WARNING:
Filtering roadmaps by milestone might not be available to you. Check the **version history** note above for details. Filtering roadmaps by milestone might not be available to you. Check the **version history** note above for details.

View File

@ -49,6 +49,7 @@ PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name
| `package_name` | string | yes | The package name. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). | `package_name` | string | yes | The package name. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`).
| `package_version` | string | yes | The package version. It can contain only numbers (`0-9`), and dots (`.`). Must be in the format of `X.Y.Z`, i.e. should match `/\A\d+\.\d+\.\d+\z/` regular expression. | `package_version` | string | yes | The package version. It can contain only numbers (`0-9`), and dots (`.`). Must be in the format of `X.Y.Z`, i.e. should match `/\A\d+\.\d+\.\d+\z/` regular expression.
| `file_name` | string | yes | The filename. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). | `file_name` | string | yes | The filename. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`).
| `status` | string | no | The package status. It can be `default` (default) or `hidden`. Hidden packages do not appear in the UI or [package API list endpoints](../../../api/packages.md).
Provide the file context in the request body. Provide the file context in the request body.

View File

@ -7,6 +7,8 @@ module API
file_name: API::NO_SLASH_URL_PART_REGEX file_name: API::NO_SLASH_URL_PART_REGEX
}.freeze }.freeze
ALLOWED_STATUSES = %w[default hidden].freeze
feature_category :package_registry feature_category :package_registry
before do before do
@ -35,6 +37,7 @@ module API
requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true
requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
optional :status, type: String, values: ALLOWED_STATUSES, desc: 'Package status'
end end
put 'authorize' do put 'authorize' do
@ -49,6 +52,7 @@ module API
requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true
requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
optional :status, type: String, values: ALLOWED_STATUSES, desc: 'Package status'
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
end end

View File

@ -33,12 +33,14 @@ module API
desc: 'Return packages with this name' desc: 'Return packages with this name'
optional :include_versionless, type: Boolean, optional :include_versionless, type: Boolean,
desc: 'Returns packages without a version' desc: 'Returns packages without a version'
optional :status, type: String, values: Packages::Package.statuses.keys,
desc: 'Return packages with specified status'
end end
get ':id/packages' do get ':id/packages' do
packages = Packages::GroupPackagesFinder.new( packages = Packages::GroupPackagesFinder.new(
current_user, current_user,
user_group, user_group,
declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless) declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless, :status)
).execute ).execute
present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true

View File

@ -32,11 +32,13 @@ module API
desc: 'Return packages with this name' desc: 'Return packages with this name'
optional :include_versionless, type: Boolean, optional :include_versionless, type: Boolean,
desc: 'Returns packages without a version' desc: 'Returns packages without a version'
optional :status, type: String, values: Packages::Package.statuses.keys,
desc: 'Return packages with specified status'
end end
get ':id/packages' do get ':id/packages' do
packages = ::Packages::PackagesFinder.new( packages = ::Packages::PackagesFinder.new(
user_project, user_project,
declared_params.slice(:order_by, :sort, :package_type, :package_name, :include_versionless) declared_params.slice(:order_by, :sort, :package_type, :package_name, :include_versionless, :status)
).execute ).execute
present paginate(packages), with: ::API::Entities::Package, user: current_user present paginate(packages), with: ::API::Entities::Package, user: current_user

View File

@ -7,9 +7,6 @@ module Gitlab
class UrlBlocker class UrlBlocker
BlockedUrlError = Class.new(StandardError) BlockedUrlError = Class.new(StandardError)
GETADDRINFO_TIMEOUT_SECONDS = 15
private_constant :GETADDRINFO_TIMEOUT_SECONDS
class << self class << self
# Validates the given url according to the constraints specified by arguments. # Validates the given url according to the constraints specified by arguments.
# #
@ -113,7 +110,7 @@ module Gitlab
end end
def get_address_info(uri, dns_rebind_protection) def get_address_info(uri, dns_rebind_protection)
Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM, timeout: GETADDRINFO_TIMEOUT_SECONDS).map do |addr| Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end end
rescue SocketError rescue SocketError

View File

@ -18536,6 +18536,9 @@ msgstr ""
msgid "Merge request (MR) approvals" msgid "Merge request (MR) approvals"
msgstr "" msgstr ""
msgid "Merge request approval settings have been updated."
msgstr ""
msgid "Merge request approvals" msgid "Merge request approvals"
msgstr "" msgstr ""

View File

@ -6,6 +6,15 @@ FactoryBot.define do
name { 'my/company/app/my-app' } name { 'my/company/app/my-app' }
sequence(:version) { |n| "1.#{n}-SNAPSHOT" } sequence(:version) { |n| "1.#{n}-SNAPSHOT" }
package_type { :maven } package_type { :maven }
status { :default }
trait :hidden do
status { :hidden }
end
trait :processing do
status { :processing }
end
factory :maven_package do factory :maven_package do
maven_metadatum maven_metadatum

View File

@ -147,6 +147,7 @@ RSpec.describe Packages::GroupPackagesFinder do
end end
it_behaves_like 'concerning versionless param' it_behaves_like 'concerning versionless param'
it_behaves_like 'concerning package statuses'
end end
context 'group has package of all types' do context 'group has package of all types' do

View File

@ -82,5 +82,6 @@ RSpec.describe ::Packages::PackagesFinder do
end end
it_behaves_like 'concerning versionless param' it_behaves_like 'concerning versionless param'
it_behaves_like 'concerning package statuses'
end end
end end

View File

@ -1,32 +0,0 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import Tab from '~/vue_shared/components/tabs/tab.vue';
describe('Tab component', () => {
const Component = Vue.extend(Tab);
let vm;
beforeEach(() => {
vm = mountComponent(Component);
});
it('sets localActive to equal active', (done) => {
vm.active = true;
vm.$nextTick(() => {
expect(vm.localActive).toBe(true);
done();
});
});
it('sets active class', (done) => {
vm.active = true;
vm.$nextTick(() => {
expect(vm.$el.classList).toContain('active');
done();
});
});
});

View File

@ -1,61 +0,0 @@
import Vue from 'vue';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import Tabs from '~/vue_shared/components/tabs/tabs';
describe('Tabs component', () => {
let vm;
beforeEach(() => {
vm = new Vue({
components: {
Tabs,
Tab,
},
render(h) {
return h('div', [
h('tabs', [
h('tab', { attrs: { title: 'Testing', active: true } }, 'First tab'),
h('tab', [h('template', { slot: 'title' }, 'Test slot'), 'Second tab']),
]),
]);
},
}).$mount();
return vm.$nextTick();
});
describe('tab links', () => {
it('renders links for tabs', () => {
expect(vm.$el.querySelectorAll('a').length).toBe(2);
});
it('renders link titles from props', () => {
expect(vm.$el.querySelector('a').textContent).toContain('Testing');
});
it('renders link titles from slot', () => {
expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
});
it('renders active class', () => {
expect(vm.$el.querySelector('a').classList).toContain('active');
});
it('updates active class on click', () => {
vm.$el.querySelectorAll('a')[1].click();
return vm.$nextTick(() => {
expect(vm.$el.querySelector('a').classList).not.toContain('active');
expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
});
});
});
describe('content', () => {
it('renders content panes', () => {
expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
});
});
});

View File

@ -160,34 +160,6 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end end
end end
end end
context 'when resolving runs into a timeout' do
let(:import_url) { 'http://example.com' }
subject { described_class.validate!(import_url, dns_rebind_protection: dns_rebind_protection) }
before do
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError)
end
context 'with dns rebinding enabled' do
let(:dns_rebind_protection) { true }
it 'raises an error due to DNS timeout' do
expect { subject }.to raise_error(described_class::BlockedUrlError)
end
end
context 'with dns rebinding disabled' do
let(:dns_rebind_protection) { false }
it_behaves_like 'validates URI and hostname' do
let(:expected_uri) { import_url }
let(:expected_hostname) { nil }
end
end
end
end end
describe '#blocked_url?' do describe '#blocked_url?' do

View File

@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe Packages::Package, type: :model do RSpec.describe Packages::Package, type: :model do
include SortingHelper include SortingHelper
it_behaves_like 'having unique enum values'
describe 'relationships' do describe 'relationships' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:creator) } it { is_expected.to belong_to(:creator) }
@ -605,6 +607,28 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to match_array([pypi_package]) } it { is_expected.to match_array([pypi_package]) }
end end
describe '.displayable' do
let_it_be(:hidden_package) { create(:maven_package, :hidden) }
let_it_be(:processing_package) { create(:maven_package, :processing) }
subject { described_class.displayable }
it 'does not include hidden packages', :aggregate_failures do
is_expected.not_to include(hidden_package)
is_expected.not_to include(processing_package)
end
end
describe '.with_status' do
let_it_be(:hidden_package) { create(:maven_package, :hidden) }
subject { described_class.with_status(:hidden) }
it 'returns packages with specified status' do
is_expected.to match_array([hidden_package])
end
end
end end
describe '.select_distinct_name' do describe '.select_distinct_name' do

View File

@ -281,6 +281,7 @@ RSpec.describe API::GenericPackages do
package = project.packages.generic.last package = project.packages.generic.last
expect(package.name).to eq('mypackage') expect(package.name).to eq('mypackage')
expect(package.status).to eq('default')
expect(package.version).to eq('0.0.1') expect(package.version).to eq('0.0.1')
if should_set_build_info if should_set_build_info
@ -293,6 +294,39 @@ RSpec.describe API::GenericPackages do
expect(package_file.file_name).to eq('myfile.tar.gz') expect(package_file.file_name).to eq('myfile.tar.gz')
end end
end end
context 'with a status' do
context 'valid status' do
let(:params) { super().merge(status: 'hidden') }
it 'assigns the status to the package' do
headers = workhorse_headers.merge(auth_header)
upload_file(params, headers)
aggregate_failures do
expect(response).to have_gitlab_http_status(:created)
package = project.packages.find_by(name: 'mypackage')
expect(package).to be_hidden
end
end
end
context 'invalid status' do
let(:params) { super().merge(status: 'processing') }
it 'rejects the package' do
headers = workhorse_headers.merge(auth_header)
upload_file(params, headers)
aggregate_failures do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
end end
context 'when valid personal access token is used' do context 'when valid personal access token is used' do

View File

@ -144,6 +144,7 @@ RSpec.describe API::GroupPackages do
end end
it_behaves_like 'with versionless packages' it_behaves_like 'with versionless packages'
it_behaves_like 'with status param'
it_behaves_like 'does not cause n^2 queries' it_behaves_like 'does not cause n^2 queries'
end end
end end

View File

@ -120,6 +120,7 @@ RSpec.describe API::ProjectPackages do
end end
it_behaves_like 'with versionless packages' it_behaves_like 'with versionless packages'
it_behaves_like 'with status param'
it_behaves_like 'does not cause n^2 queries' it_behaves_like 'does not cause n^2 queries'
end end
end end

View File

@ -43,6 +43,7 @@ RSpec.describe Packages::Composer::CreatePackageService do
end end
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
end end
context 'with a tag' do context 'with a tag' do
@ -66,6 +67,7 @@ RSpec.describe Packages::Composer::CreatePackageService do
end end
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
end end
end end

View File

@ -31,6 +31,7 @@ RSpec.describe Packages::Conan::CreatePackageService do
it_behaves_like 'assigns the package creator' it_behaves_like 'assigns the package creator'
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
end end
context 'invalid params' do context 'invalid params' do

View File

@ -13,6 +13,8 @@ RSpec.describe Packages::Generic::CreatePackageFileService do
let(:temp_file) { Tempfile.new("test") } let(:temp_file) { Tempfile.new("test") }
let(:file) { UploadedFile.new(temp_file.path, sha256: sha256) } let(:file) { UploadedFile.new(temp_file.path, sha256: sha256) }
let(:package) { create(:generic_package, project: project) } let(:package) { create(:generic_package, project: project) }
let(:package_service) { double }
let(:params) do let(:params) do
{ {
package_name: 'mypackage', package_name: 'mypackage',
@ -23,31 +25,34 @@ RSpec.describe Packages::Generic::CreatePackageFileService do
} }
end end
let(:package_params) do
{
name: params[:package_name],
version: params[:package_version],
build: params[:build],
status: nil
}
end
subject { described_class.new(project, user, params).execute } subject { described_class.new(project, user, params).execute }
before do before do
FileUtils.touch(temp_file) FileUtils.touch(temp_file)
expect(::Packages::Generic::FindOrCreatePackageService).to receive(:new).with(project, user, package_params).and_return(package_service)
expect(package_service).to receive(:execute).and_return(package)
end end
after do after do
FileUtils.rm_f(temp_file) FileUtils.rm_f(temp_file)
end end
it 'creates package file' do it 'creates package file', :aggregate_failures do
package_service = double
package_params = {
name: params[:package_name],
version: params[:package_version],
build: params[:build]
}
expect(::Packages::Generic::FindOrCreatePackageService).to receive(:new).with(project, user, package_params).and_return(package_service)
expect(package_service).to receive(:execute).and_return(package)
expect { subject }.to change { package.package_files.count }.by(1) expect { subject }.to change { package.package_files.count }.by(1)
.and change { Packages::PackageFileBuildInfo.count }.by(1) .and change { Packages::PackageFileBuildInfo.count }.by(1)
package_file = package.package_files.last package_file = package.package_files.last
aggregate_failures do aggregate_failures do
expect(package_file.package.status).to eq('default')
expect(package_file.package).to eq(package) expect(package_file.package).to eq(package)
expect(package_file.file_name).to eq('myfile.tar.gz.1') expect(package_file.file_name).to eq('myfile.tar.gz.1')
expect(package_file.size).to eq(file.size) expect(package_file.size).to eq(file.size)
@ -55,6 +60,21 @@ RSpec.describe Packages::Generic::CreatePackageFileService do
end end
end end
context 'with a status' do
let(:params) { super().merge(status: 'hidden') }
let(:package_params) { super().merge(status: 'hidden') }
it 'updates an existing packages status' do
expect { subject }.to change { package.package_files.count }.by(1)
.and change { Packages::PackageFileBuildInfo.count }.by(1)
package_file = package.package_files.last
aggregate_failures do
expect(package_file.package.status).to eq('hidden')
end
end
end
it_behaves_like 'assigns build to package file' it_behaves_like 'assigns build to package file'
end end
end end

View File

@ -36,10 +36,11 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
expect(pkg.version).to eq(version) expect(pkg.version).to eq(version)
end end
context 'with a build' do context 'with optional attributes' do
subject { service.execute.payload[:package] } subject { service.execute.payload[:package] }
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
end end
end end

View File

@ -53,6 +53,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
let(:params) { super().merge(build: job) } let(:params) { super().merge(build: job) }
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
it 'creates a package file build info' do it 'creates a package file build info' do
expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1) expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)

View File

@ -32,5 +32,6 @@ RSpec.describe Packages::Nuget::CreatePackageService do
it_behaves_like 'assigns the package creator' it_behaves_like 'assigns the package creator'
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
end end
end end

View File

@ -52,6 +52,7 @@ RSpec.describe Packages::Pypi::CreatePackageService do
end end
it_behaves_like 'assigns build to package' it_behaves_like 'assigns build to package'
it_behaves_like 'assigns status to package'
context 'with an existing package' do context 'with an existing package' do
before do before do

View File

@ -12,38 +12,19 @@ module DnsHelpers
end end
def stub_all_dns! def stub_all_dns!
allow(Addrinfo).to receive(:getaddrinfo).and_return([]) allow(Addrinfo).to receive(:getaddrinfo).with(anything, anything, nil, :STREAM).and_return([])
allow(Addrinfo).to receive(:getaddrinfo).with(anything, anything, nil, :STREAM, anything, anything).and_return([])
end end
def stub_invalid_dns! def stub_invalid_dns!
invalid_addresses = %r{ allow(Addrinfo).to receive(:getaddrinfo).with(/\Afoobar\.\w|(\d{1,3}\.){4,}\d{1,3}\z/i, anything, nil, :STREAM) do
\A raise SocketError.new("getaddrinfo: Name or service not known")
(?: end
foobar\.\w |
(?:\d{1,3}\.){4,}\d{1,3}
)
\z
}ix
allow(Addrinfo).to receive(:getaddrinfo)
.with(invalid_addresses, any_args)
.and_raise(SocketError, 'getaddrinfo: Name or service not known')
end end
def permit_local_dns! def permit_local_dns!
local_addresses = %r{ local_addresses = /\A(127|10)\.0\.0\.\d{1,3}|(192\.168|172\.16)\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost\z/i
\A allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original
(?: allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original
(?:127|10)\.0\.0\.\d{1,3} |
(?:192\.168|172\.16)\.\d{1,3}\.\d{1,3} |
0\.0\.0\.0 |
localhost
)
\z
}ix
allow(Addrinfo).to receive(:getaddrinfo)
.with(local_addresses, any_args)
.and_call_original
end end
end end

View File

@ -18,13 +18,14 @@ module StubRequests
end end
def stub_dns(url, ip_address:, port: 80) def stub_dns(url, ip_address:, port: 80)
url = URI(url) url = parse_url(url)
socket = Socket.sockaddr_in(port, ip_address) socket = Socket.sockaddr_in(port, ip_address)
addr = Addrinfo.new(socket) addr = Addrinfo.new(socket)
# See Gitlab::UrlBlocker
allow(Addrinfo).to receive(:getaddrinfo) allow(Addrinfo).to receive(:getaddrinfo)
.with(url.hostname, url.port, any_args) .with(url.hostname, url.port, nil, :STREAM)
.and_return([addr]) .and_return([addr])
end end
def stub_all_dns(url, ip_address:) def stub_all_dns(url, ip_address:)
@ -33,14 +34,22 @@ module StubRequests
socket = Socket.sockaddr_in(port, ip_address) socket = Socket.sockaddr_in(port, ip_address)
addr = Addrinfo.new(socket) addr = Addrinfo.new(socket)
# See Gitlab::UrlBlocker
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
allow(Addrinfo).to receive(:getaddrinfo) allow(Addrinfo).to receive(:getaddrinfo)
.with(url.hostname, any_args) .with(url.hostname, anything, nil, :STREAM)
.and_return([addr]) .and_return([addr])
end end
def stubbed_hostname(url, hostname: IP_ADDRESS_STUB) def stubbed_hostname(url, hostname: IP_ADDRESS_STUB)
url = URI(url) url = parse_url(url)
url.hostname = hostname url.hostname = hostname
url.to_s url.to_s
end end
private
def parse_url(url)
url.is_a?(URI) ? url : URI(url)
end
end end

View File

@ -17,3 +17,23 @@ RSpec.shared_examples 'concerning versionless param' do
it { is_expected.not_to include(versionless_package) } it { is_expected.not_to include(versionless_package) }
end end
end end
RSpec.shared_examples 'concerning package statuses' do
let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) }
context 'hidden packages' do
it { is_expected.not_to include(hidden_package) }
end
context 'with status param' do
let(:params) { { status: :hidden } }
it { is_expected.to match_array([hidden_package]) }
end
context 'with invalid status param' do
let(:params) { { status: 'invalid_status' } }
it { expect { subject }.to raise_exception(described_class::InvalidStatusError) }
end
end

View File

@ -40,6 +40,19 @@ RSpec.shared_examples 'assigns the package creator' do
end end
end end
RSpec.shared_examples 'assigns status to package' do
context 'with status param' do
let_it_be(:status) { 'hidden' }
let(:params) { super().merge(status: status) }
it 'assigns the status to the package' do
package = subject
expect(package.status).to eq(status)
end
end
end
RSpec.shared_examples 'returns packages' do |container_type, user_type| RSpec.shared_examples 'returns packages' do |container_type, user_type|
context "for #{user_type}" do context "for #{user_type}" do
before do before do
@ -263,3 +276,41 @@ RSpec.shared_examples 'with versionless packages' do
end end
end end
end end
RSpec.shared_examples 'with status param' do
context 'hidden packages' do
let!(:hidden_package) { create(:maven_package, :hidden, project: project) }
shared_examples 'not including the hidden package' do
it 'does not return the package' do
subject
expect(json_response.map { |package| package['id'] }).not_to include(hidden_package.id)
end
end
context 'no status param' do
it_behaves_like 'not including the hidden package'
end
context 'with hidden status param' do
let(:params) { super().merge(status: 'hidden') }
it 'returns the package' do
subject
expect(json_response.map { |package| package['id'] }).to include(hidden_package.id)
end
end
end
context 'bad status param' do
let(:params) { super().merge(status: 'invalid') }
it 'returns the package' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end