提交 927df95c 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 e5731d51
......@@ -208,17 +208,6 @@ Naming/RescuedExceptionsVariableName:
RSpec/ContextWording:
Enabled: false
RSpec/EmptyLineAfterSharedExample:
Exclude:
- 'ee/spec/mailers/notify_spec.rb'
- 'ee/spec/services/quick_actions/interpret_service_spec.rb'
- 'spec/controllers/repositories/git_http_controller_spec.rb'
- 'spec/finders/projects/serverless/functions_finder_spec.rb'
- 'spec/lib/gitlab/hook_data/issuable_builder_spec.rb'
- 'spec/lib/gitlab/legacy_github_import/importer_spec.rb'
- 'spec/models/event_spec.rb'
- 'spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb'
# Offense count: 879
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
......
......@@ -148,7 +148,7 @@ export default {
},
},
methods: {
filterALertsByStatus(tabIndex) {
filterAlertsByStatus(tabIndex) {
this.statusFilter = this.$options.statusTabs[tabIndex].filters;
},
capitalizeFirstCharacter,
......@@ -184,7 +184,7 @@ export default {
{{ $options.i18n.errorMsg }}
</gl-alert>
<gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterALertsByStatus">
<gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterAlertsByStatus">
<gl-tab v-for="tab in $options.statusTabs" :key="tab.status">
<template slot="title">
<span>{{ tab.title }}</span>
......
......@@ -74,20 +74,27 @@ function initStatusTriggers() {
}
}
function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
const { trackLabel, trackProperty } = elToTrack.dataset;
$(el).on('shown.bs.dropdown', () => {
Tracking.event(document.body.dataset.page, trackEvent, {
label: trackLabel,
property: trackProperty,
});
});
}
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-ci-minutes-link');
const upgradeEl = document.querySelector('.js-upgrade-plan-link');
if (el && buyEl) {
const { trackLabel, trackProperty } = buyEl.dataset;
const trackEvent = 'show_buy_ci_minutes';
trackShowUserDropdownLink('show_buy_ci_minutes', buyEl, el);
}
$(el).on('shown.bs.dropdown', () => {
Tracking.event(undefined, trackEvent, {
label: trackLabel,
property: trackProperty,
});
});
if (el && upgradeEl) {
trackShowUserDropdownLink('show_upgrade_link', upgradeEl, el);
}
}
......
......@@ -228,13 +228,11 @@ export default {
'promVariables',
'isUpdatingStarredValue',
]),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() {
return this.allDashboards.length > 0 ? this.allDashboards[0] : {};
},
selectedDashboard() {
return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
},
...mapGetters('monitoringDashboard', [
'selectedDashboard',
'getMetricStates',
'filteredEnvironments',
]),
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
},
......@@ -242,7 +240,10 @@ export default {
return (
this.customMetricsAvailable &&
!this.showEmptyState &&
this.firstDashboard === this.selectedDashboard
// Custom metrics only avaialble on system dashboards because
// they are stored in the database. This can be improved. See:
// https://gitlab.com/gitlab-org/gitlab/-/issues/28241
this.selectedDashboard?.system_dashboard
);
},
shouldShowEnvironmentsDropdownNoMatchedMsg() {
......@@ -269,7 +270,7 @@ export default {
},
expandedPanel: {
handler({ group, panel }) {
const dashboardPath = this.currentDashboard || this.firstDashboard.path;
const dashboardPath = this.currentDashboard || this.selectedDashboard?.path;
updateHistory({
url: panelToUrl(dashboardPath, group, panel),
title: document.title,
......@@ -341,7 +342,7 @@ export default {
this.selectedTimeRange = defaultTimeRange;
},
generatePanelUrl(groupKey, panel) {
const dashboardPath = this.currentDashboard || this.firstDashboard.path;
const dashboardPath = this.currentDashboard || this.selectedDashboard?.path;
return panelToUrl(dashboardPath, groupKey, panel);
},
hideAddMetricModal() {
......@@ -597,7 +598,10 @@ export default {
</gl-modal>
</div>
<div v-if="selectedDashboard.can_edit" class="mb-2 mr-2 d-flex d-sm-block">
<div
v-if="selectedDashboard && selectedDashboard.can_edit"
class="mb-2 mr-2 d-flex d-sm-block"
>
<gl-deprecated-button
class="flex-grow-1 js-edit-link"
:href="selectedDashboard.project_blob_path"
......
......@@ -14,7 +14,9 @@ const metricsIdsInPanel = panel =>
export const selectedDashboard = state => {
const { allDashboards } = state;
return (
allDashboards.find(({ path }) => path === state.currentDashboard) || allDashboards[0] || null
allDashboards.find(d => d.path === state.currentDashboard) ||
allDashboards.find(d => d.default) ||
null
);
};
......
<script>
import { mapActions } from 'vuex';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
export default {
components: {
timeAgoTooltip,
GitlabTeamMemberBadge,
GitlabTeamMemberBadge: () =>
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
},
props: {
author: {
......@@ -62,9 +62,6 @@ export default {
hasAuthor() {
return this.author && Object.keys(this.author).length;
},
showGitlabTeamMemberBadge() {
return this.author?.is_gitlab_employee;
},
authorLinkClasses() {
return {
hover: this.isUsernameLinkHovered,
......@@ -156,7 +153,7 @@ export default {
@mouseleave="handleUsernameMouseLeave"
><span class="note-headline-light">@{{ author.username }}</span>
</a>
<gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
<gitlab-team-member-badge v-if="author && author.is_gitlab_employee" />
</span>
</template>
<span v-else>{{ __('A deleted user') }}</span>
......
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
export default {
name: 'GitlabTeamMemberBadge',
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlIcon },
gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
};
</script>
<template>
<span
v-gl-tooltip.hover
:title="$options.gitlabTeamMemberLabel"
role="img"
:aria-label="$options.gitlabTeamMemberLabel"
class="d-inline-block align-middle"
>
<gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
</span>
</template>
......@@ -553,6 +553,7 @@
vertical-align: text-top;
}
a.upgrade-plan-link gl-emoji,
a.ci-minutes-emoji gl-emoji,
a.trial-link gl-emoji {
font-size: $gl-font-size;
......
# frozen_string_literal: true
class FreezePeriodsFinder
def initialize(project, current_user = nil)
@project = project
@current_user = current_user
end
def execute
return Ci::FreezePeriod.none unless Ability.allowed?(@current_user, :read_freeze_period, @project)
@project.freeze_periods
end
end
......@@ -5,6 +5,8 @@ module Ci
include StripAttribute
self.table_name = 'ci_freeze_periods'
default_scope { order(created_at: :asc) }
belongs_to :project, inverse_of: :freeze_periods
strip_attributes :freeze_start, :freeze_end
......
......@@ -305,9 +305,10 @@ class Group < Namespace
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
def refresh_members_authorized_projects(blocking: true)
UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
.execute(blocking: blocking)
def refresh_members_authorized_projects(blocking: true, priority: UserProjectAccessChangedService::HIGH_PRIORITY)
UserProjectAccessChangedService
.new(user_ids_for_project_authorizations)
.execute(blocking: blocking, priority: priority)
end
# rubocop: enable CodeReuse/ServiceClass
......
......@@ -171,6 +171,14 @@ class Milestone < ApplicationRecord
alias_method :group_milestone?, :group_timebox?
alias_method :project_milestone?, :project_timebox?
def parent
if group_milestone?
group
else
project
end
end
private
def milestone_format_reference(format = :iid)
......
# frozen_string_literal: true
module Ci
class FreezePeriodPolicy < BasePolicy
delegate { @subject.resource_parent }
end
end
......@@ -362,6 +362,10 @@ class ProjectPolicy < BasePolicy
enable :destroy_deploy_token
enable :read_prometheus_alerts
enable :admin_terraform_state
enable :create_freeze_period
enable :read_freeze_period
enable :update_freeze_period
enable :destroy_freeze_period
end
rule { public_project & metrics_dashboard_allowed }.policy do
......
......@@ -15,11 +15,8 @@ module Clusters
def execute
return unless management_project_required?
ActiveRecord::Base.transaction do
project = create_management_project!
update_cluster!(project)
end
project = create_management_project!
update_cluster!(project)
end
private
......
......@@ -39,6 +39,8 @@ module MergeRequests
# Don't try to print expensive instance variables.
def inspect
return "#<#{self.class}>" unless respond_to?(:merge_request)
"#<#{self.class} #{merge_request.to_reference(full: true)}>"
end
......
......@@ -108,8 +108,22 @@ module Projects
# users in the background
def setup_authorizations
if @project.group
@project.group.refresh_members_authorized_projects(blocking: false)
current_user.refresh_authorized_projects
if Feature.enabled?(:specialized_project_authorization_workers)
AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
# AuthorizedProjectsWorker uses an exclusive lease per user but
# specialized workers might have synchronization issues. Until we
# compare the inconsistency rates of both approaches, we still run
# AuthorizedProjectsWorker but with some delay and lower urgency as a
# safety net.
@project.group.refresh_members_authorized_projects(
blocking: false,
priority: UserProjectAccessChangedService::LOW_PRIORITY
)
else
@project.group.refresh_members_authorized_projects(blocking: false)
end
else
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
end
......
# frozen_string_literal: true
class UserProjectAccessChangedService
DELAY = 1.hour
HIGH_PRIORITY = :high
LOW_PRIORITY = :low
def initialize(user_ids)
@user_ids = Array.wrap(user_ids)
end
def execute(blocking: true)
def execute(blocking: true, priority: HIGH_PRIORITY)
bulk_args = @user_ids.map { |id| [id] }
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
if priority == HIGH_PRIORITY
AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
else
AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in(DELAY, bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
end
end
end
end
......
......@@ -27,6 +27,7 @@
%li
= link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' }
= render_if_exists 'layouts/header/buy_ci_minutes', project: @project, namespace: @group
= render_if_exists 'layouts/header/upgrade'
- if current_user_menu?(:help)
%li.divider.d-md-none
......
......@@ -11,5 +11,5 @@
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md new-gl-button js-close-callout'
%button.gl-banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' }
'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box') }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
......@@ -18,7 +18,7 @@
= http_clone_button(project)
= render_if_exists 'shared/kerberos_clone_button', project: project
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Project clone URL') }
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
......@@ -14,4 +14,4 @@
%li.js-builds-dropdown-loading.hidden
.loading-container.text-center
%span.spinner{ 'aria-label': 'Loading' }
%span.spinner{ 'aria-label': _('Loading') }
- if show_no_ssh_key_message?
%div{ class: 'no-ssh-key-message gl-alert gl-alert-warning', role: 'alert' }
= sprite_icon('warning', size: 16, css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
%button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': 'Dismiss' }
%button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon s16')
.gl-alert-body
= s_("MissingSSHKeyWarningLink|You won't be able to pull or push project code via SSH until you add an SSH key to your profile").html_safe
......
......@@ -4,20 +4,20 @@
%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened'), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
= link_to page_filter_path(state: 'opened'), id: 'state-opened', title: _("Filter by %{page_context_word} that are currently opened.") % { page_context_word: page_context_word }, data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged'), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
= link_to page_filter_path(state: 'merged'), id: 'state-merged', title: _('Filter by merge requests that are currently merged.'), data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged, display_count)}
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
= link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed', qa_selector: 'closed_issues_link' } do
= link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
---
title: Skip mergeability check when listing MRs in the API
merge_request: 31890
author:
type: performance
---
title: Fix bug in Groups API when statistics are requested in an unauthenticated
API call
merge_request: 32057
author:
type: fixed
---
title: Externalize i18n strings from ./app/views/shared/issuable/_nav.html.haml
merge_request: 32165
author: Gilang Gumilar
type: changed
---
title: Externalize i18n aria-label strings from ./app/views/shared/*
merge_request: 32142
author: Gilang Gumilar
type: changed
---
title: Expose Freeze Periods in REST API
merge_request: 29382
author:
type: added
---
title: Fix database schema inconsistency with not-null checks
merge_request: 31930
author:
type: other
# frozen_string_literal: true
class ChangeVerificationChecksumFieldTypeInPackageFile < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
# The use of this column is behind a feature flag that never got enabled,
# so it's safe to remove it in a normal migration
remove_column :packages_package_files, :verification_checksum, :string # rubocop:disable Migration/RemoveColumn
add_column :packages_package_files, :verification_checksum, :binary
end
def down
remove_column :packages_package_files, :verification_checksum, :binary
add_column :packages_package_files, :verification_checksum, :string
end
end
# frozen_string_literal: true
class AddChecksumIndexToPackageFile < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :packages_package_files, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "packages_packages_verification_checksum_partial"
end
def down
remove_concurrent_index :packages_package_files, :verification_checksum
end
end
# frozen_string_literal: true
class FixNotNullCheckConstraintInconsistency < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
table = :application_settings
%i(container_registry_vendor container_registry_version).each do |column|
change_column_null table, column, false
remove_not_null_constraint(table, column) if check_not_null_constraint_exists?(table, column)
end
end
def down
# No-op: for regular systems without the inconsistency, #up is a no-op, too
end
end
......@@ -4668,9 +4668,9 @@ CREATE TABLE public.packages_package_files (
file_sha256 bytea,
verification_retry_at timestamp with time zone,
verified_at timestamp with time zone,
verification_checksum character varying(255),
verification_failure character varying(255),
verification_retry_count integer
verification_retry_count integer,
verification_checksum bytea
);
CREATE SEQUENCE public.packages_package_files_id_seq
......@@ -13738,6 +13738,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200408175424
20200408212219
20200409085956
20200409105455
20200409105456
20200409211607
20200410104828
20200410232012
......@@ -13817,6 +13819,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200511162115
20200512085150
20200512164334
20200513160930
20200513234502
20200513235347
20200513235532
......
......@@ -8,7 +8,7 @@ This article expands on [How to Configure LDAP with GitLab CE](../how_to_configu
## GitLab Enterprise Edition - LDAP features
[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has a number of advantages when it comes to integrating with Active Directory (LDAP):
[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has several advantages when it comes to integrating with Active Directory (LDAP):
- [Administrator Sync](../ldap-ee.md#administrator-sync): As an extension of group sync, you can automatically manage your global GitLab administrators. Specify a group CN for `admin_group` and all members of the LDAP group will be given administrator privileges.
- [Group Sync](#group-sync): This allows GitLab group membership to be automatically updated based on LDAP group members.
......@@ -16,7 +16,7 @@ This article expands on [How to Configure LDAP with GitLab CE](../how_to_configu
- Daily user synchronization: Once a day, GitLab will run a synchronization to check and update GitLab users against LDAP. This process updates all user details automatically.
On the following section, you'll find a description for each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
In the following section, you'll find a description of each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
![GitLab OU Structure](img/admin_group.png)
......@@ -28,7 +28,7 @@ Group syncing allows AD (LDAP) groups to be mapped to GitLab groups. This provid
#### Creating group links - example
As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, it is important that users of this group are given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, users of this group must be given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
**UK Developers** of our "UKGov" group are given **"developer"** permissions.
......
......@@ -150,9 +150,9 @@ Project.update_all(visibility_level: 0)
#
projects = Project.where(pending_delete: true)
projects.each do |p|
puts "Project name: #{p.id}"
puts "Project ID: #{p.id}"
puts "Project name: #{p.name}"
puts "Repository path: #{p.repository.storage_path}"
puts "Repository path: #{p.repository.full_path}"
end
#
......
......@@ -47,6 +47,7 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
......@@ -64,6 +65,13 @@ Parameters:
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
NOTE: **Note:**
[Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890),
listing merge requests may not proactively update the `merge_status` field
(which also affects the `has_conflicts` field), as this can be an expensive
operation. If you are interested in the value of these fields from this
endpoint, set the `with_merge_status_recheck` parameter to `true` in the query.
NOTE: **Note:**
[Starting in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/29984),
when `async_merge_request_check_mergeability` feature flag is enabled, the
......@@ -227,6 +235,7 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
......@@ -390,6 +399,7 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413)|
| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
......
......@@ -96,11 +96,16 @@ with [domain expertise](#domain-experts).
1. If your merge request includes documentation changes, it must be **approved
by a [Technical writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers)**, based on
the appropriate [product category](https://about.gitlab.com/handbook/product/categories/).
1. If your merge request includes Quality and non-Quality-related changes (*3*), it must be **approved
by a [Software Engineer in Test](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors)**.
1. If your merge request includes _only_ Quality-related changes (*3*), it must be **approved
by a [Quality maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_qa)**.
- (*1*): Please note that specs other than JavaScript specs are considered backend code.
- (*2*): We encourage you to seek guidance from a database maintainer if your merge
request is potentially introducing expensive queries. It is most efficient to comment
on the line of code in question with the SQL queries so they can give their advice.
- (*3*): Quality-related changes include all files within the `qa` directory.
#### Security requirements
......@@ -320,6 +325,7 @@ Before taking the decision to merge:
- Consider warnings and errors from danger bot, code quality, and other reports.
Unless a strong case can be made for the violation, these should be resolved
before merging. A comment must to be posted if the MR is merged with any failed job.
- If the MR contains both Quality and non-Quality-related changes, the MR should be merged by the relevant maintainer for user-facing changes (backend, frontend, or database) after the Quality related changes are approved by a Software Engineer in Test.
When ready to merge:
......
......@@ -324,7 +324,7 @@ Widgets should now be replicated by Geo!
def change
add_column :widgets, :verification_retry_at, :datetime_with_timezone
add_column :widgets, :verified_at, :datetime_with_timezone
add_column :widgets, :verification_checksum, :string
add_column :widgets, :verification_checksum, :binary, using: 'verification_checksum::bytea'
add_column :widgets, :verification_failure, :string
add_column :widgets, :verification_retry_count, :integer
end
......
......@@ -23,10 +23,10 @@ and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
- `double` and `spy` are faster than `FactoryBot.build(...)`
- `instance_double` and `spy` are faster than `FactoryBot.build(...)`
- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
`spy`, or `instance_double` will do. Database persistence is slow!
- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
......
......@@ -19,7 +19,7 @@ as the hardware requirements that are needed to install and use GitLab.
- Scientific Linux (please use the CentOS packages and instructions)
- Oracle Linux (please use the CentOS packages and instructions)
For the installations options, see [the main installation page](README.md).
For the installation options, see [the main installation page](README.md).
### Unsupported Linux distributions and Unix-like operating systems
......@@ -68,7 +68,7 @@ GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets, which
version of Node.js 10.13.0.
You can check which version you are running with `node -v`. If you are running
a version older than `v10.13.0`, you need to update to a newer version. You
a version older than `v10.13.0`, you need to update it to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the [Node.js website](https://nodejs.org/en/download/).
......@@ -126,7 +126,7 @@ available when needed.
Our [Memory Team](https://about.gitlab.com/handbook/engineering/development/enablement/memory/) is actively working to reduce the memory requirement.
NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need for those.
## Database
......
......@@ -8,6 +8,12 @@ description: "The static site editor enables users to edit content on static web
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10.
> - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0.
DANGER: **Danger:**
In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282)
to the URL structure of the Static Site Editor. Follow the instructions in this
[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539)
to update your project with the latest changes.
Static Site Editor enables users to edit content on static websites without
prior knowledge of the underlying templating language, site architecture, or
Git commands. A contributor to your project can quickly edit a Markdown page
......
......@@ -148,7 +148,7 @@ snippet was created using the GitLab web interface the original line ending is W
> Introduced in GitLab 10.8.
Public snippets can not only be shared, but also embedded on any website. This
allows to reuse a GitLab snippet in multiple places and any change to the source
allows us to reuse a GitLab snippet in multiple places and any change to the source
is automatically reflected in the embedded snippet.
To embed a snippet, first make sure that:
......@@ -172,6 +172,6 @@ Here's how an embedded snippet looks like:
<script src="https://gitlab.com/gitlab-org/gitlab-foss/snippets/1717978.js"></script>
Embedded snippets are displayed with a header that shows the file name if defined,
Embedded snippets are displayed with a header that shows the file name is defined,
the snippet size, a link to GitLab, and the actual snippet content. Actions in
the header allow users to see the snippet in raw format and download it.
......@@ -141,6 +141,7 @@ module API
mount ::API::Events
mount ::API::Features
mount ::API::Files
mount ::API::FreezePeriods
mount ::API::GroupBoards
mount ::API::GroupClusters
mount ::API::GroupExport
......
# frozen_string_literal: true
module API
module Entities
class FreezePeriod < Grape::Entity
expose :id
expose :freeze_start, :freeze_end, :cron_timezone
expose :created_at, :updated_at
end
end
end
......@@ -50,8 +50,10 @@ module API
# use `MergeRequest#mergeable?` instead (boolean).
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more
# information.
expose :merge_status do |merge_request|
merge_request.check_mergeability(async: true)
#
# For list endpoints, we skip the recheck by default, since it's expensive
expose :merge_status do |merge_request, options|
merge_request.check_mergeability(async: true) unless options[:skip_merge_status_recheck]
merge_request.public_merge_status
end
expose :diff_head_sha, as: :sha
......
# frozen_string_literal: true
module API
class FreezePeriods < Grape::API
include PaginationParams
before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get project freeze periods' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::FreezePeriod
end
params do
use :pagination
end
get ":id/freeze_periods" do
authorize! :read_freeze_period, user_project
freeze_periods = ::FreezePeriodsFinder.new(user_project, current_user).execute
present paginate(freeze_periods), with: Entities::FreezePeriod, current_user: current_user
end
desc 'Get a single freeze period' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::FreezePeriod
end
params do
requires :freeze_period_id, type: Integer, desc: 'The ID of a project freeze period'
end
get ":id/freeze_periods/:freeze_period_id" do
authorize! :read_freeze_period, user_project
present freeze_period, with: Entities::FreezePeriod, current_user: current_user
end
desc 'Create a new freeze period' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::FreezePeriod
end
params do
requires :freeze_start, type: String, desc: 'Freeze Period start'
requires :freeze_end, type: String, desc: 'Freeze Period end'
optional :cron_timezone, type: String, desc: 'Timezone'
end
post ':id/freeze_periods' do
authorize! :create_freeze_period, user_project
freeze_period_params = declared(params, include_parent_namespaces: false)
freeze_period = user_project.freeze_periods.create(freeze_period_params)
if freeze_period.persisted?
present freeze_period, with: Entities::FreezePeriod
else
render_validation_error!(freeze_period)
end
end
desc 'Update a freeze period' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::FreezePeriod
end
params do
optional :freeze_start, type: String, desc: 'Freeze Period start'
optional :freeze_end, type: String, desc: 'Freeze Period end'
optional :cron_timezone, type: String, desc: 'Freeze Period Timezone'
end
put ':id/freeze_periods/:freeze_period_id' do
authorize! :update_freeze_period, user_project
freeze_period_params = declared(params, include_parent_namespaces: false, include_missing: false)
if freeze_period.update(freeze_period_params)
present freeze_period, with: Entities::FreezePeriod
else
render_validation_error!(freeze_period)
end
end
desc 'Delete a freeze period' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::FreezePeriod
end
params do
requires :freeze_period_id, type: Integer, desc: 'Freeze Period ID'
end
delete ':id/freeze_periods/:freeze_period_id' do
authorize! :destroy_freeze_period, user_project
destroy_conditionally!(freeze_period)
end
end
helpers do
def freeze_period
@freeze_period ||= user_project.freeze_periods.find(params[:freeze_period_id])
end
end
end
end
......@@ -91,7 +91,7 @@ module API
options = {
with: Entities::Group,
current_user: current_user,
statistics: params[:statistics] && current_user.admin?
statistics: params[:statistics] && current_user&.admin?
}
groups = groups.with_statistics if options[:statistics]
......
......@@ -27,6 +27,7 @@ module API
coerce_with: Validations::Types::LabelsList.coerce,
desc: 'Comma-separated list of label names'
optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false
optional :with_merge_status_recheck, type: Boolean, desc: 'Request that stale merge statuses be rechecked asynchronously', default: false
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time'
......
......@@ -93,6 +93,9 @@ module API
options[:with] = Entities::MergeRequestSimple
else
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
if Feature.enabled?(:mr_list_api_skip_merge_status_recheck, default_enabled: true)
options[:skip_merge_status_recheck] = !declared_params[:with_merge_status_recheck]
end
end
options
......
......@@ -13,6 +13,8 @@ module ContainerRegistry
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'
REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version'
REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
......@@ -24,6 +26,21 @@ module ContainerRegistry
@options = options
end
def registry_info
response = faraday.get("/v2/")
return {} unless response&.success?
version = response.headers[REGISTRY_VERSION_HEADER]
features = response.headers.fetch(REGISTRY_FEATURES_HEADER, '')
{
version: version,
features: features.split(',').map(&:strip),
vendor: version ? 'gitlab' : 'other'
}
end
def repository_tags(name)
response_body faraday.get("/v2/#{name}/tags/list")
end
......
......@@ -42,6 +42,9 @@ module Gitlab
},
buy_ci_minutes_version_a: {
tracking_category: 'Growth::Expansion::Experiment::BuyCiMinutesVersionA'
},
upgrade_link_in_user_menu_a: {
tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA'
}
}.freeze
......
......@@ -57,11 +57,8 @@ module Gitlab
# Already a commit?
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
# Some weird thing?
return unless commit_id.is_a?(String)
# This saves us an RPC round trip.
return if commit_id.include?(':')
return unless valid?(commit_id)
commit = find_commit(repo, commit_id)
......@@ -431,6 +428,15 @@ module Gitlab
def fetch_body_from_gitaly
self.class.get_message(@repository, id)
end
def self.valid?(commit_id)
commit_id.is_a?(String) && !(
commit_id.start_with?('-') ||
commit_id.include?(':') ||
commit_id.include?("\x00") ||
commit_id.match?(/\s/)
)
end
end
end
end
......
......@@ -145,7 +145,8 @@ module Gitlab
services_usage,
usage_counters,
user_preferences_usage,
ingress_modsecurity_usage
ingress_modsecurity_usage,
container_expiration_policies_usage
)
}
end
......@@ -185,31 +186,8 @@ module Gitlab
web_ide_clientside_preview_enabled: alt_usage_data { Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? },
ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity),
grafana_link_enabled: alt_usage_data { Gitlab::CurrentSettings.grafana_enabled? }
}.merge(features_usage_data_container_expiration_policies)
end
# rubocop: disable CodeReuse/ActiveRecord
def features_usage_data_container_expiration_policies
results = {}
start = ::Project.minimum(:id)
finish = ::Project.maximum(:id)
results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
base = ::ContainerExpirationPolicy.active
results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
%i[keep_n cadence older_than].each do |option|
::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
end
end
results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
results
}
end
# rubocop: enable CodeReuse/ActiveRecord
# @return [Hash<Symbol, Integer>]
def usage_counters
......@@ -315,6 +293,29 @@ module Gitlab
}
end
# rubocop: disable CodeReuse/ActiveRecord
def container_expiration_policies_usage
results = {}
start = ::Project.minimum(:id)
finish = ::Project.maximum(:id)
results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
base = ::ContainerExpirationPolicy.active
results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
%i[keep_n cadence older_than].each do |option|
::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
end
end
results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
results
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
results = Service.available_services_names.without('jira').each_with_object({}) do |service_name, response|
......
......@@ -2978,6 +2978,9 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Dismiss Auto DevOps box"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
......@@ -3506,6 +3509,9 @@ msgstr ""
msgid "BurndownChartLabel|Open issues"
msgstr ""
msgid "Burnup chart"
msgstr ""
msgid "Business"
msgstr ""
......@@ -6471,6 +6477,9 @@ msgstr ""
msgid "CurrentUser|Start a Gold trial"
msgstr ""
msgid "CurrentUser|Upgrade"
msgstr ""
msgid "Custom CI configuration path"
msgstr ""
......@@ -9443,9 +9452,21 @@ msgstr ""
msgid "Filter by %{issuable_type} that are currently opened."
msgstr ""
msgid "Filter by %{page_context_word} that are currently opened."
msgstr ""
msgid "Filter by commit message"
msgstr ""
msgid "Filter by issues that are currently closed."
msgstr ""
msgid "Filter by merge requests that are currently closed and unmerged."
msgstr ""
msgid "Filter by merge requests that are currently merged."
msgstr ""
msgid "Filter by milestone name"
msgstr ""
......@@ -16276,6 +16297,9 @@ msgstr ""
msgid "Project cannot be shared with the group it is in or one of its ancestors."
msgstr ""
msgid "Project clone URL"
msgstr ""
msgid "Project configuration, including services"
msgstr ""
......@@ -24603,6 +24627,9 @@ msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
msgid "You do not have an active license"
msgstr ""
msgid "You do not have any subscriptions yet"
msgstr ""
......@@ -24651,6 +24678,9 @@ msgstr ""
msgid "You don’t have access to Value Stream Analytics for this group"
msgstr ""
msgid "You have a license(s) that activates at a future date. Please see the License History table below."
msgstr ""
msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
msgstr ""
......
......@@ -169,6 +169,7 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { project.owner }
end
it_behaves_like 'git_upload_pack behavior', true
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccess }
......@@ -183,6 +184,7 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { personal_snippet.author }
end
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
......@@ -197,6 +199,7 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { project_snippet.author }
end
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
......
# frozen_string_literal: true
require 'spec_helper'
describe FreezePeriodsFinder do
subject(:finder) { described_class.new(project, user).execute }
let(:project) { create(:project, :private) }
let(:user) { create(:user) }
let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
shared_examples_for 'returns nothing' do
specify do
is_expected.to be_empty
end
end
shared_examples_for 'returns freeze_periods ordered by created_at asc' do
it 'returns freeze_periods ordered by created_at' do
expect(subject.count).to eq(2)
expect(subject.pluck('id')).to eq([freeze_period_1.id, freeze_period_2.id])
end
end
context 'when user is a maintainer' do
before do
project.add_maintainer(user)
end
it_behaves_like 'returns freeze_periods ordered by created_at asc'
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like 'returns nothing'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like 'returns nothing'
end
context 'when user is not a project member' do
it_behaves_like 'returns nothing'
context 'when project is public' do
let(:project) { create(:project, :public) }
it_behaves_like 'returns nothing'
end
end
end
......@@ -48,6 +48,7 @@ describe Projects::Serverless::FunctionsFinder do
expect(function_finder.knative_installed).to be false
end
end
context 'when project level cluster is present and enabled' do
it_behaves_like 'before first deployment' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: true) }
......
{
"type": "object",
"required": [
"id",
"freeze_start",
"freeze_end",
"cron_timezone",
"created_at",
"updated_at"
],
"properties": {
"id": { "type": "integer" },
"freeze_start": { "type": "string" },
"freeze_end": { "type": "string"},
"cron_timezone": { "type": "string" },
"created_at": { "type": "string" },
"updated_at": { "type": "string" }
},
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "freeze_period.json" }
}
......@@ -60,8 +60,8 @@ describe('Header', () => {
beforeEach(() => {
setFixtures(`
<li class="js-nav-user-dropdown">
<a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes
</a>
<a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes</a>
<a class="js-upgrade-plan-link" data-track-event="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a>
</li>`);
trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn);
......@@ -77,8 +77,16 @@ describe('Header', () => {
it('sends a tracking event when the dropdown is opened and contains Buy CI minutes link', () => {
$('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'show_buy_ci_minutes', {
expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_buy_ci_minutes', {
label: 'free',
property: 'user_dropdown',
});
});
it('sends a tracking event when the dropdown is opened and contains Upgrade link', () => {
$('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_upgrade_link', {
label: 'free',
property: 'user_dropdown',
});
......
......@@ -19,6 +19,7 @@ import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
setupAllDashboards,
setupStoreWithDashboard,
setMetricResult,
setupStoreWithData,
......@@ -279,7 +280,7 @@ describe('Dashboard', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(), // state
expect.any(String), // document title
expect.stringContaining(`?${expectedSearch}`),
expect.stringContaining(`${expectedSearch}`),
);
});
});
......@@ -302,7 +303,7 @@ describe('Dashboard', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(), // state
expect.any(String), // document title
expect.stringContaining(`?${expectedSearch}`),
expect.stringContaining(`${expectedSearch}`),
);
});
});
......@@ -317,7 +318,7 @@ describe('Dashboard', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(), // state
expect.any(String), // document title
expect.not.stringContaining('?'), // no params
expect.not.stringMatching(/group|title|y_label/), // no panel params
);
});
});
......@@ -359,6 +360,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper();
setupAllDashboards(store);
});
it('toggle star button is shown', () => {
......@@ -380,10 +382,7 @@ describe('Dashboard', () => {
const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
beforeEach(() => {
wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
dashboardGitResponse,
);
setupAllDashboards(store);
jest.spyOn(store, 'dispatch');
});
......@@ -400,7 +399,9 @@ describe('Dashboard', () => {
describe('when dashboard is not starred', () => {
beforeEach(() => {
wrapper.setProps({ currentDashboard: dashboardGitResponse[0].path });
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[0].path,
});
return wrapper.vm.$nextTick();
});
......@@ -415,7 +416,9 @@ describe('Dashboard', () => {
describe('when dashboard is starred', () => {
beforeEach(() => {
wrapper.setProps({ currentDashboard: dashboardGitResponse[1].path });
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboardGitResponse[1].path,
});
return wrapper.vm.$nextTick();
});
......@@ -551,7 +554,7 @@ describe('Dashboard', () => {
it('sets a link to the expanded panel', () => {
const searchQuery =
'?group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
'?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
expect.stringContaining(searchQuery),
......@@ -808,10 +811,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
dashboardGitResponse,
);
setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
......@@ -820,10 +820,11 @@ describe('Dashboard', () => {
});
it('is present for a custom dashboard, and links to its edit_path', () => {
const dashboard = dashboardGitResponse[1]; // non-default dashboard
const currentDashboard = dashboard.path;
const dashboard = dashboardGitResponse[1];
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard: dashboard.path,
});
wrapper.setProps({ currentDashboard });
return wrapper.vm.$nextTick().then(() => {
expect(findEditLink().exists()).toBe(true);
expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
......@@ -834,12 +835,7 @@ describe('Dashboard', () => {
describe('Dashboard dropdown', () => {
beforeEach(() => {
createMountedWrapper({ hasMetrics: true });
wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
dashboardGitResponse,
);
setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
......@@ -872,7 +868,7 @@ describe('Dashboard', () => {
});
describe('Clipboard text in panels', () => {
const currentDashboard = 'TEST_DASHBOARD';
const currentDashboard = dashboardGitResponse[1].path;
const panelIndex = 1; // skip expanded panel
const getClipboardTextFirstPanel = () =>
......@@ -882,37 +878,20 @@ describe('Dashboard', () => {
.props('clipboardText');
beforeEach(() => {
setupStoreWithData(store);
createShallowWrapper({ hasMetrics: true, currentDashboard });
setupStoreWithData(wrapper.vm.$store);
return wrapper.vm.$nextTick();
});
it('contains a link to the dashboard', () => {
expect(getClipboardTextFirstPanel()).toContain(`dashboard=${currentDashboard}`);
const dashboardParam = `dashboard=${encodeURIComponent(currentDashboard)}`;
expect(getClipboardTextFirstPanel()).toContain(dashboardParam);
expect(getClipboardTextFirstPanel()).toContain(`group=`);
expect(getClipboardTextFirstPanel()).toContain(`title=`);
expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
it('strips the undefined parameter', () => {
wrapper.setProps({ currentDashboard: undefined });
return wrapper.vm.$nextTick(() => {
expect(getClipboardTextFirstPanel()).not.toContain(`dashboard=`);
expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
});
it('null parameter is stripped', () => {
wrapper.setProps({ currentDashboard: null });
return wrapper.vm.$nextTick(() => {
expect(getClipboardTextFirstPanel()).not.toContain(`dashboard=`);
expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
});
});
describe('add custom metrics', () => {
......
......@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { setupAllDashboards } from '../store_utils';
import { propsData } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
......@@ -15,6 +16,8 @@ describe('Dashboard template', () => {
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
setupAllDashboards(store);
});
afterEach(() => {
......
import * as types from '~/monitoring/stores/mutation_types';
import { metricsResult, environmentData } from './mock_data';
import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
import { metricsDashboardPayload } from './fixture_data';
export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => {
......@@ -16,11 +16,19 @@ const setEnvironmentData = $store => {
$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
};
export const setupAllDashboards = $store => {
$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
};
export const setupStoreWithDashboard = $store => {
$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload,
);
$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload,
);
};
export const setupStoreWithVariable = $store => {
......@@ -30,6 +38,7 @@ export const setupStoreWithVariable = $store => {
};
export const setupStoreWithData = $store => {
setupAllDashboards($store);
setupStoreWithDashboard($store);
setMetricResult({ $store, result: [], panel: 0 });
......
......@@ -2,7 +2,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import NoteHeader from '~/notes/components/note_header.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -141,20 +140,6 @@ describe('NoteHeader component', () => {
});
});
test.each`
props | expected | message1 | message2
${{ author: { ...author, is_gitlab_employee: true } }} | ${true} | ${'renders'} | ${'true'}
${{ author: { ...author, is_gitlab_employee: false } }} | ${false} | ${"doesn't render"} | ${'false'}
${{ author }} | ${false} | ${"doesn't render"} | ${'undefined'}
`(
'$message1 GitLab team member badge when `is_gitlab_employee` is $message2',
({ props, expected }) => {
createComponent(props);
expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected);
},
);
describe('loading spinner', () => {
it('shows spinner when showSpinner is true', () => {
createComponent();
......
......@@ -20,7 +20,7 @@ describe('Release detail mutations', () => {
release = convertObjectPropsToCamelCase(originalRelease);
});
describe(types.REQUEST_RELEASE, () => {
describe(`${types.REQUEST_RELEASE}`, () => {
it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](state);
......@@ -28,7 +28,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.RECEIVE_RELEASE_SUCCESS, () => {
describe(`${types.RECEIVE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_RELEASE_SUCCESS](state, release);
......@@ -42,7 +42,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.RECEIVE_RELEASE_ERROR, () => {
describe(`${types.RECEIVE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_RELEASE_ERROR](state, error);
......@@ -55,7 +55,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.UPDATE_RELEASE_TITLE, () => {
describe(`${types.UPDATE_RELEASE_TITLE}`, () => {
it("updates the release's title", () => {
state.release = release;
const newTitle = 'The new release title';
......@@ -65,7 +65,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.UPDATE_RELEASE_NOTES, () => {
describe(`${types.UPDATE_RELEASE_NOTES}`, () => {
it("updates the release's notes", () => {
state.release = release;
const newNotes = 'The new release notes';
......@@ -75,7 +75,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.REQUEST_UPDATE_RELEASE, () => {
describe(`${types.REQUEST_UPDATE_RELEASE}`, () => {
it('set state.isUpdatingRelease to true', () => {
mutations[types.REQUEST_UPDATE_RELEASE](state);
......@@ -83,7 +83,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
......@@ -93,7 +93,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
......@@ -104,7 +104,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.ADD_EMPTY_ASSET_LINK, () => {
describe(`${types.ADD_EMPTY_ASSET_LINK}`, () => {
it('adds a new, empty link object to the release', () => {
state.release = release;
......@@ -123,7 +123,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.UPDATE_ASSET_LINK_URL, () => {
describe(`${types.UPDATE_ASSET_LINK_URL}`, () => {
it('updates an asset link with a new URL', () => {
state.release = release;
......@@ -138,7 +138,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.UPDATE_ASSET_LINK_NAME, () => {
describe(`${types.UPDATE_ASSET_LINK_NAME}`, () => {
it('updates an asset link with a new name', () => {
state.release = release;
......@@ -153,7 +153,7 @@ describe('Release detail mutations', () => {
});
});
describe(types.REMOVE_ASSET_LINK, () => {
describe(`${types.REMOVE_ASSET_LINK}`, () => {
it('removes an asset link from the release', () => {
state.release = release;
......
......@@ -85,7 +85,7 @@ describe ContainerRegistry::Client do
it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do
stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345")
.with(headers: blob_headers)
.to_return(status: 307, body: "", headers: { Location: 'http://redirected' })
.to_return(status: 307, body: '', headers: { Location: 'http://redirected' })
# We should probably use hash_excluding here, but that requires an update to WebMock:
# https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb
stub_request(:get, "http://redirected/")
......@@ -238,4 +238,54 @@ describe ContainerRegistry::Client do
it { is_expected.to be_falsey }
end
end
def stub_registry_info(headers: {}, status: 200)
stub_request(:get, 'http://container-registry/v2/')
.to_return(status: status, body: "", headers: headers)
end
describe '#registry_info' do
subject { client.registry_info }
context 'when the check is successful' do
context 'when using the GitLab container registry' do
before do
stub_registry_info(headers: {
'GitLab-Container-Registry-Version' => '2.9.1-gitlab',
'GitLab-Container-Registry-Features' => 'a,b,c'
})
end
it 'identifies the vendor as "gitlab"' do
expect(subject).to include(vendor: 'gitlab')
end
it 'identifies version and features' do
expect(subject).to include(version: '2.9.1-gitlab', features: %w[a b c])
end
end
context 'when using a third-party container registry' do
before do
stub_registry_info
end
it 'identifies the vendor as "other"' do
expect(subject).to include(vendor: 'other')
end
it 'does not identify version or features' do
expect(subject).to include(version: nil, features: [])
end
end
end
context 'when the check is not successful' do
it 'does not identify vendor, version or features' do
stub_registry_info(status: 500)
expect(subject).to eq({})
end
end
end
end
......@@ -161,6 +161,26 @@ describe Gitlab::Git::Commit, :seed_helper do
expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
it "returns nil for id started with dash" do
expect(described_class.find(repository, "-HEAD")).to be_nil
end
it "returns nil for id containing colon" do
expect(described_class.find(repository, "HEAD:")).to be_nil
end
it "returns nil for id containing space" do
expect(described_class.find(repository, "HE AD")).to be_nil
end
it "returns nil for id containing tab" do
expect(described_class.find(repository, "HE\tAD")).to be_nil
end
it "returns nil for id containing NULL" do
expect(described_class.find(repository, "HE\x00AD")).to be_nil
end
context 'with broken repo' do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') }
......
......@@ -12,6 +12,7 @@ describe Gitlab::HookData::IssuableBuilder do
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
......
......@@ -294,6 +294,7 @@ describe Gitlab::LegacyGithubImport::Importer do
it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute' do
let(:expected_not_called) { [:import_releases, [:import_comments, :pull_requests]] }
end
it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
......
......@@ -99,6 +99,46 @@ describe Gitlab::UsageData, :aggregate_failures do
)
end
context 'with existing container expiration policies' do
let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
%i[keep_n cadence older_than].each do |attribute|
ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
end
end
let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
let(:active_policies) { ::ContainerExpirationPolicy.active }
subject { described_class.data[:counts] }
it 'gathers usage data' do
expect(subject[:projects_with_expiration_policy_enabled]).to eq 20
expect(subject[:projects_with_expiration_policy_disabled]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 14
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 16
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 12
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 5
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
end
end
it 'works when queries time out' do
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
......@@ -192,43 +232,6 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:grafana_link_enabled]).to eq(false)
end
end
context 'with existing container expiration policies' do
let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
%i[keep_n cadence older_than].each do |attribute|
ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
end
end
let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
let(:active_policies) { ::ContainerExpirationPolicy.active }
it 'gathers usage data' do
expect(subject[:projects_with_expiration_policy_enabled]).to eq 16
expect(subject[:projects_with_expiration_policy_disabled]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 10
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 12
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 12
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
end
end
end
describe '#components_usage_data' do
......
......@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Ci::FreezePeriod, type: :model do
subject { build(:ci_freeze_period) }
let(:invalid_cron) { '0 0 0 * *' }
it { is_expected.to belong_to(:project) }
it { is_expected.to respond_to(:freeze_start) }
......@@ -13,13 +15,19 @@ RSpec.describe Ci::FreezePeriod, type: :model do
describe 'cron validations' do
it 'allows valid cron patterns' do
freeze_period = build(:ci_freeze_period, freeze_start: '0 23 * * 5')
freeze_period = build(:ci_freeze_period)
expect(freeze_period).to be_valid
end
it 'does not allow invalid cron patterns' do
freeze_period = build(:ci_freeze_period, freeze_start: '0 0 0 * *')
it 'does not allow invalid cron patterns on freeze_start' do
freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron)
expect(freeze_period).not_to be_valid
end
it 'does not allow invalid cron patterns on freeze_end' do
freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron)
expect(freeze_period).not_to be_valid
end
......
......@@ -300,6 +300,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to assignee and author', true
end
......@@ -309,6 +310,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
include_examples 'visible to assignee and author', true
end
end
......@@ -320,6 +322,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to assignee and author', true
end
......@@ -329,6 +332,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
include_examples 'visible to assignee and author', true
end
......@@ -429,6 +433,7 @@ describe Event do
end
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
include_examples 'visible to author', false
end
......@@ -440,6 +445,7 @@ describe Event do
end
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
include_examples 'visible to author', false
end
end
......@@ -450,6 +456,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
include_examples 'visible to author', true
context 'on internal snippet' do
......@@ -466,6 +473,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:admin) }
end
include_examples 'visible to author', true
end
end
......
......@@ -480,4 +480,22 @@ describe Milestone do
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
end
describe '#parent' do
context 'with group' do
it 'returns the expected parent' do
group = create(:group)
expect(build(:milestone, group: group).parent).to eq(group)
end
end
context 'with project' do
it 'returns the expected parent' do
project = create(:project)
expect(build(:milestone, project: project).parent).to eq(project)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::FreezePeriods do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let(:api_user) { user }
let(:invalid_cron) { '0 0 0 * *' }
let(:last_freeze_period) { project.freeze_periods.last }
describe 'GET /projects/:id/freeze_periods' do
context 'when the user is the admin' do
let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
it 'returns 200 HTTP status' do
get api("/projects/#{project.id}/freeze_periods", admin)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when the user is the maintainer' do
before do
project.add_maintainer(user)
end
context 'when there are two freeze_periods' do
let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
it 'returns 200 HTTP status' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns freeze_periods ordered by created_at ascending' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(json_response.count).to eq(2)
expect(freeze_period_ids).to eq([freeze_period_1.id, freeze_period_2.id])
end
it 'matches response schema' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(response).to match_response_schema('public_api/v4/freeze_periods')
end
end
context 'when there are no freeze_periods' do
it 'returns 200 HTTP status' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns an empty response' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(json_response).to be_empty
end
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
let!(:freeze_period) do
create(:ci_freeze_period, project: project)
end
it 'responds 403 Forbidden' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'responds 404 Not Found' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :public) }
it 'responds 403 Forbidden' do
get api("/projects/#{project.id}/freeze_periods", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
describe 'GET /projects/:id/freeze_periods/:freeze_period_id' do
context 'when there is a freeze period' do
let!(:freeze_period) do
create(:ci_freeze_period, project: project)
end
context 'when the user is the admin' do
let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
it 'responds 200 OK' do
get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", admin)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when the user is the maintainer' do
before do
project.add_maintainer(user)
end
it 'responds 200 OK' do
get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns a freeze period' do
get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
expect(json_response).to include(
'id' => freeze_period.id,
'freeze_start' => freeze_period.freeze_start,
'freeze_end' => freeze_period.freeze_end,
'cron_timezone' => freeze_period.cron_timezone)
end
it 'matches response schema' do
get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
expect(response).to match_response_schema('public_api/v4/freeze_period')
end
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it 'responds 403 Forbidden' do
get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'when project is public' do
let(:project) { create(:project, :public) }
context 'when freeze_period exists' do
it 'responds 403 Forbidden' do
get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when freeze_period does not exist' do
it 'responds 403 Forbidden' do
get api("/projects/#{project.id}/freeze_periods/0", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
end
end
describe 'POST /projects/:id/freeze_periods' do
let(:params) do
{
freeze_start: '0 23 * * 5',
freeze_end: '0 7 * * 1',
cron_timezone: 'UTC'
}
end
subject { post api("/projects/#{project.id}/freeze_periods", api_user), params: params }
context 'when the user is the admin' do
let(:api_user) { admin }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:created)
end
end
context 'when user is the maintainer' do
before do
project.add_maintainer(user)
end
context 'with valid params' do
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:created)
end
it 'creates a new freeze period' do
expect do
subject
end.to change { Ci::FreezePeriod.count }.by(1)
expect(last_freeze_period.freeze_start).to eq('0 23 * * 5')
expect(last_freeze_period.freeze_end).to eq('0 7 * * 1')
expect(last_freeze_period.cron_timezone).to eq('UTC')
end
it 'matches response schema' do
subject
expect(response).to match_response_schema('public_api/v4/freeze_period')
end
end
context 'with incomplete params' do
let(:params) do
{
freeze_start: '0 23 * * 5',
cron_timezone: 'UTC'
}
end
it 'responds 400 Bad Request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq("freeze_end is missing")
end
end
context 'with invalid params' do
let(:params) do
{
freeze_start: '0 23 * * 5',
freeze_end: invalid_cron,
cron_timezone: 'UTC'
}
end
it 'responds 400 Bad Request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['freeze_end']).to eq([" is invalid syntax"])
end
end
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :public) }
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
describe 'PUT /projects/:id/freeze_periods/:freeze_period_id' do
let(:params) { { freeze_start: '0 22 * * 5', freeze_end: '5 4 * * sun' } }
let!(:freeze_period) { create :ci_freeze_period, project: project }
subject { put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user), params: params }
context 'when user is the admin' do
let(:api_user) { admin }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when user is the maintainer' do
before do
project.add_maintainer(user)
end
context 'with valid params' do
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'performs the update' do
subject
freeze_period.reload
expect(freeze_period.freeze_start).to eq(params[:freeze_start])
expect(freeze_period.freeze_end).to eq(params[:freeze_end])
end
it 'matches response schema' do
subject
expect(response).to match_response_schema('public_api/v4/freeze_period')
end
end
context 'with invalid params' do
let(:params) { { freeze_start: invalid_cron } }
it 'responds 400 Bad Request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['freeze_start']).to eq([" is invalid syntax"])
end
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'responds 404 Not Found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :public) }
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
describe 'DELETE /projects/:id/freeze_periods/:freeze_period_id' do
let!(:freeze_period) { create :ci_freeze_period, project: project }
let(:freeze_period_id) { freeze_period.id }
subject { delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user) }
context 'when user is the admin' do
let(:api_user) { admin }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when user is the maintainer' do
before do
project.add_maintainer(user)
end
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:no_content)
end
it 'destroys the freeze period' do
expect do
subject
end.to change { Ci::FreezePeriod.count }.by(-1)
end
context 'when it is a non-existing freeze period id' do
let(:freeze_period_id) { 0 }
it '404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'responds 404 Not Found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :public) }
it 'responds 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
def freeze_period_ids
json_response.map do |freeze_period_hash|
freeze_period_hash.fetch('id')&.to_i
end
end
end
......@@ -6,15 +6,15 @@ describe API::Groups do
include GroupAPIHelpers
include UploadHelpers
let(:user1) { create(:user, can_create_group: false) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
let!(:group2) { create(:group, :private) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
let_it_be(:user1) { create(:user, can_create_group: false) }
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
let_it_be(:group2) { create(:group, :private) }
let_it_be(:project1) { create(:project, namespace: group1) }
let_it_be(:project2) { create(:project, namespace: group2) }
let_it_be(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
......@@ -90,6 +90,17 @@ describe API::Groups do
get api("/groups", admin)
end.not_to exceed_query_limit(control)
end
context 'when statistics are requested' do
it 'does not include statistics' do
get api("/groups"), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
end
end
context "when authenticated as user" do
......@@ -1113,6 +1124,17 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when statistics are requested' do
it 'does not include statistics' do
get api("/groups/#{group1.id}/subgroups"), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
end
end
context 'when authenticated as user' do
......
......@@ -66,17 +66,36 @@ describe API::MergeRequests do
end
context 'when merge request is unchecked' do
let(:check_service_class) { MergeRequests::MergeabilityCheckService }
let(:mr_entity) { json_response.find { |mr| mr['id'] == merge_request.id } }
before do
merge_request.mark_as_unchecked!
end
it 'checks mergeability asynchronously' do
expect_next_instance_of(MergeRequests::MergeabilityCheckService) do |service|
expect(service).not_to receive(:execute)
expect(service).to receive(:async_execute)
context 'with merge status recheck projection' do
it 'checks mergeability asynchronously' do
expect_next_instance_of(check_service_class) do |service|
expect(service).not_to receive(:execute)
expect(service).to receive(:async_execute).and_call_original
end
get(api(endpoint_path, user), params: { with_merge_status_recheck: true })
expect_successful_response_with_paginated_array
expect(mr_entity['merge_status']).to eq('checking')
end
end
get api(endpoint_path, user)
context 'without merge status recheck projection' do
it 'does not enqueue a merge status recheck' do
expect(check_service_class).not_to receive(:new)
get api(endpoint_path, user)
expect_successful_response_with_paginated_array
expect(mr_entity['merge_status']).to eq('unchecked')
end
end
end
......
......@@ -510,6 +510,83 @@ describe Projects::CreateService, '#execute' do
end
end
context 'with specialized_project_authorization_workers' do
let_it_be(:other_user) { create(:user) }
let_it_be(:group) { create(:group) }
let(:opts) do
{
name: 'GitLab',
namespace_id: group.id
}
end
before do
group.add_maintainer(user)
group.add_developer(other_user)
end
it 'updates authorization for current_user' do
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user).and_call_original
)
project = create_project(user, opts)
expect(
Ability.allowed?(user, :read_project, project)
).to be_truthy
end
it 'schedules authorization update for users with access to group' do
expect(AuthorizedProjectsWorker).not_to(
receive(:bulk_perform_async)
)
expect(AuthorizedProjectUpdate::ProjectCreateWorker).to(
receive(:perform_async).and_call_original
)
expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
receive(:bulk_perform_in)
.with(1.hour, array_including([user.id], [other_user.id]))
.and_call_original
)
create_project(user, opts)
end
context 'when feature is disabled' do
before do
stub_feature_flags(specialized_project_authorization_workers: false)
end
it 'updates authorization for current_user' do
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user).and_call_original
)
project = create_project(user, opts)
expect(
Ability.allowed?(user, :read_project, project)
).to be_truthy
end
it 'uses AuthorizedProjectsWorker' do
expect(AuthorizedProjectsWorker).to(
receive(:bulk_perform_async).with(array_including([user.id], [other_user.id])).and_call_original
)
expect(AuthorizedProjectUpdate::ProjectCreateWorker).not_to(
receive(:perform_async)
)
expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to(
receive(:bulk_perform_in)
)
create_project(user, opts)
end
end
end
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
......
......@@ -17,5 +17,14 @@ describe UserProjectAccessChangedService do
described_class.new([1, 2]).execute(blocking: false)
end
it 'permits low-priority operation' do
expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
receive(:bulk_perform_in).with(described_class::DELAY, [[1], [2]])
)
described_class.new([1, 2]).execute(blocking: false,
priority: described_class::LOW_PRIORITY)
end
end
end
......@@ -99,6 +99,24 @@ module UsageDataHelpers
projects_with_error_tracking_enabled
projects_with_alerts_service_enabled
projects_with_prometheus_alerts
projects_with_expiration_policy_enabled
projects_with_expiration_policy_disabled
projects_with_expiration_policy_enabled_with_keep_n_unset
projects_with_expiration_policy_enabled_with_keep_n_set_to_1
projects_with_expiration_policy_enabled_with_keep_n_set_to_5
projects_with_expiration_policy_enabled_with_keep_n_set_to_10
projects_with_expiration_policy_enabled_with_keep_n_set_to_25
projects_with_expiration_policy_enabled_with_keep_n_set_to_50
projects_with_expiration_policy_enabled_with_older_than_unset
projects_with_expiration_policy_enabled_with_older_than_set_to_7d
projects_with_expiration_policy_enabled_with_older_than_set_to_14d
projects_with_expiration_policy_enabled_with_older_than_set_to_30d
projects_with_expiration_policy_enabled_with_older_than_set_to_90d
projects_with_expiration_policy_enabled_with_cadence_set_to_1d
projects_with_expiration_policy_enabled_with_cadence_set_to_7d
projects_with_expiration_policy_enabled_with_cadence_set_to_14d
projects_with_expiration_policy_enabled_with_cadence_set_to_1month
projects_with_expiration_policy_enabled_with_cadence_set_to_3month
pages_domains
protected_branches
releases
......@@ -136,25 +154,6 @@ module UsageDataHelpers
prometheus_metrics_enabled
web_ide_clientside_preview_enabled
ingress_modsecurity_enabled
projects_with_expiration_policy_disabled
projects_with_expiration_policy_enabled
projects_with_expiration_policy_enabled_with_keep_n_unset
projects_with_expiration_policy_enabled_with_older_than_unset
projects_with_expiration_policy_enabled_with_keep_n_set_to_1
projects_with_expiration_policy_enabled_with_keep_n_set_to_5
projects_with_expiration_policy_enabled_with_keep_n_set_to_10
projects_with_expiration_policy_enabled_with_keep_n_set_to_25
projects_with_expiration_policy_enabled_with_keep_n_set_to_50
projects_with_expiration_policy_enabled_with_keep_n_set_to_100
projects_with_expiration_policy_enabled_with_cadence_set_to_1d
projects_with_expiration_policy_enabled_with_cadence_set_to_7d
projects_with_expiration_policy_enabled_with_cadence_set_to_14d
projects_with_expiration_policy_enabled_with_cadence_set_to_1month
projects_with_expiration_policy_enabled_with_cadence_set_to_3month
projects_with_expiration_policy_enabled_with_older_than_set_to_7d
projects_with_expiration_policy_enabled_with_older_than_set_to_14d
projects_with_expiration_policy_enabled_with_older_than_set_to_30d
projects_with_expiration_policy_enabled_with_older_than_set_to_90d
object_store
).freeze
......
# frozen_string_literal: true
shared_examples 'error tracking index page' do
it 'renders the error index page' do
it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.js-title-container') do
expect(page).to have_content(project.namespace.name)
expect(page).to have_content(project.name)
......@@ -15,7 +15,7 @@ shared_examples 'error tracking index page' do
end
end
it 'loads the error show page on click' do
it 'loads the error show page on click', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
click_on issues_response[0]['title']
wait_for_requests
......@@ -23,7 +23,7 @@ shared_examples 'error tracking index page' do
expect(page).to have_content('Error Details')
end
it 'renders the error index data' do
it 'renders the error index data', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.error-list') do
expect(page).to have_content(issues_response[0]['title'])
expect(page).to have_content(issues_response[0]['count'].to_s)
......@@ -34,7 +34,7 @@ shared_examples 'error tracking index page' do
end
shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1|
it 'expands the stack trace context' do
it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.stacktrace') do
find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line
......@@ -49,7 +49,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_
end
shared_examples 'error tracking show page' do
it 'renders the error details' do
it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
content = page.find(".content")
nav = page.find("nav.breadcrumbs")
header = page.find(".error-details-header")
......@@ -67,11 +67,11 @@ shared_examples 'error tracking show page' do
expect(content).to have_content('Users: 0')
end
it 'renders the stack trace heading' do
it 'renders the stack trace heading', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
expect(page).to have_content('Stack trace')
end
it 'renders the stack trace', :quarantine do
it 'renders the stack trace', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame|
expect(frame['filename']).not_to be_nil
expect(page).to have_content(frame['filename'])
......
......@@ -7,6 +7,7 @@ RSpec.shared_examples 'issuable hook data' do |kind|
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
......
......@@ -787,10 +787,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.127.0.tgz#1f7ffdffe44d6a82b372535f93d78f3a895d1960"
integrity sha512-Uv52DqkG2KwCB0VRlXUEHFZxJ/7Ql0t1YTdzICpXmDjltuUBrysFcdmWPVO6PgXQxk2ahryNsUjSOziMYTeSiw==
"@gitlab/ui@14.5.0":
version "14.5.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.5.0.tgz#6fed9a5b435884fe69e499578469c8b144726c90"
integrity sha512-7OarJzyyeRpFRmShN7c2GBPpahBDbmOSk10ATisannbX/h9i+Z83MQ8ZDqYbM8qeRIfG/BVsnLjC8M7aSsBlPQ==
"@gitlab/ui@14.10.0":
version "14.10.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.10.0.tgz#39c04d62c914fcefe96c7ec32fdf31b1f98f1119"
integrity sha512-k9w6z3/QBeUas++cH5BaozjxY4fVu+AggjGoh9QMKN5hpiGTiTPx5aQJIlOv8UX/kpUmgc4pHSWAbw30YVGGFw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册