Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-01 18:17:05 +00:00
parent 3bdc719293
commit 143a33345c
52 changed files with 641 additions and 249 deletions

View File

@ -524,7 +524,7 @@ export default {
if ( if (
window.gon?.features?.diffsVirtualScrolling || window.gon?.features?.diffsVirtualScrolling ||
window.gon?.features?.diffSearchingUsageData window.gon?.features?.usageDataDiffSearches
) { ) {
let keydownTime; let keydownTime;
Mousetrap.bind(['mod+f', 'mod+g'], () => { Mousetrap.bind(['mod+f', 'mod+g'], () => {
@ -540,7 +540,7 @@ export default {
if (delta >= 0 && delta < 1000) { if (delta >= 0 && delta < 1000) {
this.disableVirtualScroller(); this.disableVirtualScroller();
if (window.gon?.features?.diffSearchingUsageData) { if (window.gon?.features?.usageDataDiffSearches) {
api.trackRedisHllUserEvent('i_code_review_user_searches_diff'); api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
api.trackRedisCounterEvent('diff_searches'); api.trackRedisCounterEvent('diff_searches');
} }

View File

@ -43,9 +43,6 @@ export default {
}; };
}, },
computed: { computed: {
selectedNamespaceId() {
return this.selectedId;
},
disableSubmitButton() { disableSubmitButton() {
return this.isPaidGroup || !this.selectedId; return this.isPaidGroup || !this.selectedId;
}, },

View File

@ -4,6 +4,10 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import TransferGroupForm, { i18n } from './components/transfer_group_form.vue'; import TransferGroupForm, { i18n } from './components/transfer_group_form.vue';
const prepareGroups = (rawGroups) => { const prepareGroups = (rawGroups) => {
if (!rawGroups) {
return { group: [] };
}
const group = JSON.parse(rawGroups).map(({ id, text: humanName }) => ({ const group = JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
id, id,
humanName, humanName,
@ -22,7 +26,7 @@ export default () => {
targetFormId = null, targetFormId = null,
buttonText: confirmButtonText = '', buttonText: confirmButtonText = '',
groupName = '', groupName = '',
parentGroups = [], parentGroups,
isPaidGroup, isPaidGroup,
} = el.dataset; } = el.dataset;

View File

@ -578,7 +578,7 @@ export default {
:endpoint="mr.accessibilityReportPath" :endpoint="mr.accessibilityReportPath"
/> />
<div class="mr-widget-section"> <div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" /> <component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge <ready-to-merge
v-if="isRestructuredMrWidgetEnabled && mr.commitsCount" v-if="isRestructuredMrWidgetEnabled && mr.commitsCount"

View File

@ -16,8 +16,13 @@ export const i18n = {
USERS: __('Users'), USERS: __('Users'),
}; };
const filterByName = (data, searchTerm = '') => const filterByName = (data, searchTerm = '') => {
data.filter((d) => d.humanName.toLowerCase().includes(searchTerm)); if (!searchTerm) {
return data;
}
return data.filter((d) => d.humanName.toLowerCase().includes(searchTerm.toLowerCase()));
};
export default { export default {
name: 'NamespaceSelect', name: 'NamespaceSelect',
@ -85,7 +90,15 @@ export default {
}, },
filteredEmptyNamespaceTitle() { filteredEmptyNamespaceTitle() {
const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this; const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this;
return includeEmptyNamespace && emptyNamespaceTitle.toLowerCase().includes(searchTerm);
if (!includeEmptyNamespace) {
return '';
}
if (!searchTerm) {
return emptyNamespaceTitle;
}
return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase());
}, },
}, },
methods: { methods: {

View File

@ -47,7 +47,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Usage data feature flags # Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml) push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml) push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
push_frontend_feature_flag(:diff_searching_usage_data, @project, default_enabled: :yaml) push_frontend_feature_flag(:usage_data_diff_searches, @project, default_enabled: :yaml)
end end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module Mutations
module WorkItems
class Delete < BaseMutation
description "Deletes a work item." \
" Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
graphql_name 'WorkItemDelete'
authorize :delete_work_item
argument :id, ::Types::GlobalIDType[::WorkItem],
required: true,
description: 'Global ID of the work item.'
field :project, Types::ProjectType,
null: true,
description: 'Project the deleted work item belonged to.'
def resolve(id:)
work_item = authorized_find!(id: id)
unless Feature.enabled?(:work_items, work_item.project)
return { errors: ['`work_items` feature flag disabled for this project'] }
end
result = ::WorkItems::DeleteService.new(
project: work_item.project,
current_user: current_user
).execute(work_item)
{
project: result.success? ? work_item.project : nil,
errors: result.errors
}
end
private
def find_object(id:)
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end

View File

@ -126,6 +126,7 @@ module Types
mount_mutation Mutations::Packages::DestroyFile mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Echo mount_mutation Mutations::Echo
mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items
mount_mutation Mutations::WorkItems::Delete
mount_mutation Mutations::WorkItems::Update mount_mutation Mutations::WorkItems::Update
end end
end end

View File

@ -2,4 +2,11 @@
class WorkItemPolicy < BasePolicy class WorkItemPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
desc 'User is author of the work item'
condition(:author) do
@user && @user == @subject.author
end
rule { can?(:owner_access) | author }.enable :delete_work_item
end end

View File

@ -139,10 +139,7 @@ module Projects
destroy_web_hooks! destroy_web_hooks!
destroy_project_bots! destroy_project_bots!
destroy_ci_records! destroy_ci_records!
destroy_mr_diff_commits!
if ::Feature.enabled?(:extract_mr_diff_commit_deletions, default_enabled: :yaml)
destroy_mr_diff_commits!
end
# Rails attempts to load all related records into memory before # Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510 # destroying: https://github.com/rails/rails/issues/22510

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module WorkItems
class DeleteService < Issuable::DestroyService
def execute(work_item)
unless current_user.can?(:delete_work_item, work_item)
return ::ServiceResponse.error(message: 'User not authorized to delete work item')
end
if super
::ServiceResponse.success
else
::ServiceResponse.error(message: work_item.errors.full_messages)
end
end
end
end

View File

@ -8,9 +8,7 @@
= form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
= s_('ProjectSettings|Pipelines must succeed') = s_('ProjectSettings|Pipelines must succeed')
.text-secondary .text-secondary
- configuring_pipelines_for_merge_requests_help_link_url = help_page_path('ci/pipelines/merge_request_pipelines.md', anchor: 'prerequisites') = s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
- configuring_pipelines_for_merge_requests_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configuring_pipelines_for_merge_requests_help_link_url }
= s_('ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}').html_safe % { link_start: configuring_pipelines_for_merge_requests_help_link_start, link_end: '</a>'.html_safe }
.form-check.mb-2 .form-check.mb-2
.gl-pl-6 .gl-pl-6
= form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input' = form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input'

View File

@ -6,36 +6,6 @@
- elsif note.contributor? - elsif note.contributor?
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor") %span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
.note-actions-item
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
%div
%template{ 'v-if' => 'isResolved' }
= render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' }
= render 'shared/icons/icon_resolve_discussion.svg'
- if can?(current_user, :award_emoji, note) - if can?(current_user, :award_emoji, note)
- if note.emoji_awardable? - if note.emoji_awardable?
.note-actions-item .note-actions-item

View File

@ -1 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg>

Before

Width:  |  Height:  |  Size: 409 B

View File

@ -1 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 362 B

View File

@ -1,8 +0,0 @@
---
name: extract_mr_diff_commit_deletions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75963
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347073
milestone: '14.6'
type: development
group: group::code review
default_enabled: false

View File

@ -1,5 +1,5 @@
--- ---
name: diff_searching_usage_data name: usage_data_diff_searches
introduced_by_url: introduced_by_url:
rollout_issue_url: rollout_issue_url:
milestone: '14.2' milestone: '14.2'

View File

@ -15,10 +15,12 @@ if Gitlab.ee?
else else
Gitlab::Database::Partitioning.register_tables([ Gitlab::Database::Partitioning.register_tables([
{ {
limit_connection_names: %i[main],
table_name: 'incident_management_pending_alert_escalations', table_name: 'incident_management_pending_alert_escalations',
partitioned_column: :process_at, strategy: :monthly partitioned_column: :process_at, strategy: :monthly
}, },
{ {
limit_connection_names: %i[main],
table_name: 'incident_management_pending_issue_escalations', table_name: 'incident_management_pending_issue_escalations',
partitioned_column: :process_at, strategy: :monthly partitioned_column: :process_at, strategy: :monthly
} }
@ -31,6 +33,7 @@ unless Gitlab.jh?
# This should be synchronized with the following model: # This should be synchronized with the following model:
# https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/app/models/phone/verification_code.rb # https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/app/models/phone/verification_code.rb
{ {
limit_connection_names: %i[main],
table_name: 'verification_codes', table_name: 'verification_codes',
partitioned_column: :created_at, strategy: :monthly partitioned_column: :created_at, strategy: :monthly
} }

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
#
# Zip64 is needed to support archives with more than 65535 entries.
Zip.write_zip64_support = true

View File

@ -23,7 +23,6 @@ The following lists the currently supported OSs and their possible EOL dates.
| Debian 9 | GitLab CE / GitLab EE 9.3.0 | amd64 | 2022 | <https://wiki.debian.org/LTS> | | Debian 9 | GitLab CE / GitLab EE 9.3.0 | amd64 | 2022 | <https://wiki.debian.org/LTS> |
| Debian 10 | GitLab CE / GitLab EE 12.2.0 | amd64, arm64 | 2024 | <https://wiki.debian.org/LTS> | | Debian 10 | GitLab CE / GitLab EE 12.2.0 | amd64, arm64 | 2024 | <https://wiki.debian.org/LTS> |
| Debian 11 | GitLab CE / GitLab EE 14.6.0 | amd64, arm64 | 2026 | <https://wiki.debian.org/LTS> | | Debian 11 | GitLab CE / GitLab EE 14.6.0 | amd64, arm64 | 2026 | <https://wiki.debian.org/LTS> |
| OpenSUSE 15.2 | GitLab CE / GitLab EE 13.11.0 | x86_64, aarch64 | Dec 2021 | <https://en.opensuse.org/Lifetime> |
| OpenSUSE 15.3 | GitLab CE / GitLab EE 14.5.0 | x86_64, aarch64 | Nov 2022 | <https://en.opensuse.org/Lifetime> | | OpenSUSE 15.3 | GitLab CE / GitLab EE 14.5.0 | x86_64, aarch64 | Nov 2022 | <https://en.opensuse.org/Lifetime> |
| SLES 12 | GitLab EE 9.0.0 | x86_64 | Oct 2027 | <https://www.suse.com/lifecycle/> | | SLES 12 | GitLab EE 9.0.0 | x86_64 | Oct 2027 | <https://www.suse.com/lifecycle/> |
| Ubuntu 18.04 | GitLab CE / GitLab EE 10.7.0 | amd64 | April 2023 | <https://wiki.ubuntu.com/Releases> | | Ubuntu 18.04 | GitLab CE / GitLab EE 10.7.0 | amd64 | April 2023 | <https://wiki.ubuntu.com/Releases> |
@ -81,8 +80,9 @@ release for them can be found below:
| Raspbian Stretch | [June 2020](https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/) | [GitLab CE](https://packages.gitlab.com/app/gitlab/raspberry-pi2/search?q=gitlab-ce_13.2&dist=raspbian%2Fstretch) 13.3 | | Raspbian Stretch | [June 2020](https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/) | [GitLab CE](https://packages.gitlab.com/app/gitlab/raspberry-pi2/search?q=gitlab-ce_13.2&dist=raspbian%2Fstretch) 13.3 |
| Debian Jessie | [June 2020](https://www.debian.org/News/2020/20200709) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.2&dist=debian%2Fjessie) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.2&dist=debian%2Fjessie) 13.3 | | Debian Jessie | [June 2020](https://www.debian.org/News/2020/20200709) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.2&dist=debian%2Fjessie) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.2&dist=debian%2Fjessie) 13.3 |
| CentOS 6 | [November 2020](https://wiki.centos.org/About/Product) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=13.6&filter=all&filter=all&dist=el%2F6) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=13.6&filter=all&filter=all&dist=el%2F6) 13.6 | | CentOS 6 | [November 2020](https://wiki.centos.org/About/Product) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=13.6&filter=all&filter=all&dist=el%2F6) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=13.6&filter=all&filter=all&dist=el%2F6) 13.6 |
| OpenSUSE 15.1 | [November 2020](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-13.12&dist=opensuse%2F15.1) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-13.12&dist=opensuse%2F15.2) 13.12 | | OpenSUSE 15.1 | [November 2020](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-13.12&dist=opensuse%2F15.1) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-13.12&dist=opensuse%2F15.1) 13.12 |
| Ubuntu 16.04 | [April 2021](https://ubuntu.com/info/release-end-of-life) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.12&dist=ubuntu%2Fxenial) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.12&dist=ubuntu%2Fxenial) 13.12 | | Ubuntu 16.04 | [April 2021](https://ubuntu.com/info/release-end-of-life) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce_13.12&dist=ubuntu%2Fxenial) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee_13.12&dist=ubuntu%2Fxenial) 13.12 |
| OpenSUSE 15.2 | [December 2021](https://en.opensuse.org/Lifetime#Discontinued_distributions) | [GitLab CE](https://packages.gitlab.com/app/gitlab/gitlab-ce/search?q=gitlab-ce-14.7&dist=opensuse%2F15.2) / [GitLab EE](https://packages.gitlab.com/app/gitlab/gitlab-ee/search?q=gitlab-ee-14.7&dist=opensuse%2F15.2) 14.7 |
NOTE: NOTE:
An exception to this deprecation policy is when we are unable to provide An exception to this deprecation policy is when we are unable to provide

View File

@ -5145,6 +5145,27 @@ Input type: `WorkItemCreateInput`
| <a id="mutationworkitemcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationworkitemcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationworkitemcreateworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Created work item. | | <a id="mutationworkitemcreateworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Created work item. |
### `Mutation.workItemDelete`
Deletes a work item. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
Input type: `WorkItemDeleteInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemdeleteid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationworkitemdeleteproject"></a>`project` | [`Project`](#project) | Project the deleted work item belonged to. |
### `Mutation.workItemUpdate` ### `Mutation.workItemUpdate`
Updates a work item by Global ID. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice. Updates a work item by Global ID. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.

View File

@ -6,8 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Metrics Dictionary Guide # Metrics Dictionary Guide
[Service Ping](index.md) metrics are defined in the [Service Ping](index.md) metrics are defined in individual YAML files definitions from which the
[Metrics Dictionary](https://metrics.gitlab.com/). [Metrics Dictionary](https://metrics.gitlab.com/) is built.
This guide describes the dictionary and how it's implemented. This guide describes the dictionary and how it's implemented.
## Metrics Definition and validation ## Metrics Definition and validation
@ -194,7 +194,7 @@ tier:
- ultimate - ultimate
``` ```
## Create a new metric definition ### Create a new metric definition
The GitLab codebase provides a dedicated [generator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_definition_generator.rb) to create new metric definitions. The GitLab codebase provides a dedicated [generator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_definition_generator.rb) to create new metric definitions.
@ -229,7 +229,7 @@ bundle exec rails generate gitlab:usage_metric_definition counts.issues --ee --d
create ee/config/metrics/counts_7d/issues.yml create ee/config/metrics/counts_7d/issues.yml
``` ```
## Metrics added dynamic to Service Ping payload ### Metrics added dynamic to Service Ping payload
The [Redis HLL metrics](implement.md#known-events-are-added-automatically-in-service-data-payload) are added automatically to Service Ping payload. The [Redis HLL metrics](implement.md#known-events-are-added-automatically-in-service-data-payload) are added automatically to Service Ping payload.
@ -250,3 +250,13 @@ bundle exec rails generate gitlab:usage_metric_definition:redis_hll issues users
create config/metrics/counts_7d/i_closed_weekly.yml create config/metrics/counts_7d/i_closed_weekly.yml
create config/metrics/counts_28d/i_closed_monthly.yml create config/metrics/counts_28d/i_closed_monthly.yml
``` ```
## Metrics Dictionary
[Metrics Dictionary is a separate application](https://gitlab.com/gitlab-org/growth/product-intelligence/metric-dictionary).
All metrics available in Service Ping are in the [Metrics Dictionary](https://metrics.gitlab.com/).
### Copy query to clipboard
To check if a metric has data in Sisense, use the copy query to clipboard feature. This copies a query that's ready to use in Sisense. The query gets the last five service ping data for GitLab.com for a given metric. For information about how to check if a Service Ping metric has data in Sisense, see this [demo](https://www.youtube.com/watch?v=n4o65ivta48).

View File

@ -256,7 +256,7 @@ expiration date without a gap in available service. An invoice is
generated for the renewal and available for viewing or download on the generated for the renewal and available for viewing or download on the
[View invoices](https://customers.gitlab.com/receipts) page. [View invoices](https://customers.gitlab.com/receipts) page.
#### Enable automatic renewal #### Enable or disable automatic renewal
To view or change automatic subscription renewal (at the same tier as the To view or change automatic subscription renewal (at the same tier as the
previous period), log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in), and: previous period), log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in), and:
@ -292,7 +292,7 @@ for more information.
### Purchase additional CI/CD minutes ### Purchase additional CI/CD minutes
You can [purchase additional minutes](../../ci/pipelines/cicd_minutes.md#purchase-additional-cicd-minutes) You can [purchase additional minutes](../../ci/pipelines/cicd_minutes.md#purchase-additional-cicd-minutes)
for your personal or group namespace. for your personal or group namespace. CI/CD minutes are a **one-time purchase**, so they do not renew.
## Add-on subscription for additional Storage and Transfer ## Add-on subscription for additional Storage and Transfer
@ -309,7 +309,11 @@ locked. Projects can only be unlocked by purchasing more storage subscription un
### Purchase more storage and transfer ### Purchase more storage and transfer
You can purchase storage for your personal or group namespace. You can purchase a storage subscription for your personal or group namespace.
NOTE:
Storage subscriptions **[renew automatically](#automatic-renewal) each year**.
You can [cancel the subscription](#enable-or-disable-automatic-renewal) to disable the automatic renewal.
#### For your personal namespace #### For your personal namespace

View File

@ -30,8 +30,8 @@ to Kubernetes clusters using the [GitLab Agent](../user/clusters/agent/install/i
#### GitOps deployments **(PREMIUM)** #### GitOps deployments **(PREMIUM)**
With the [GitLab Agent](../user/clusters/agent/install/index.md), you can perform pull-based With the [GitLab Agent](../user/clusters/agent/install/index.md), you can perform [pull-based
deployments using Kubernetes manifests. This provides a scalable, secure, and cloud-native deployments of Kubernetes manifests](../user/clusters/agent/repository.md#synchronize-manifest-projects). This provides a scalable, secure, and cloud-native
approach to manage Kubernetes deployments. approach to manage Kubernetes deployments.
#### Deploy to Kubernetes with the CI/CD Tunnel #### Deploy to Kubernetes with the CI/CD Tunnel

View File

@ -82,7 +82,7 @@ For more details, refer to our [architecture documentation](https://gitlab.com/g
## Install the Agent in your cluster ## Install the Agent in your cluster
See how to [install the Agent in your cluster](install/index.md). To connect your cluster to GitLab, [install the Agent on your cluster](install/index.md).
## GitOps deployments **(PREMIUM)** ## GitOps deployments **(PREMIUM)**

View File

@ -229,3 +229,16 @@ approval rule for certain branches:
![Scoped to protected branch](img/scoped_to_protected_branch_v13_10.png) ![Scoped to protected branch](img/scoped_to_protected_branch_v13_10.png)
1. To enable this configuration, read 1. To enable this configuration, read
[Code Owner's approvals for protected branches](../../protected_branches.md#require-code-owner-approval-on-a-protected-branch). [Code Owner's approvals for protected branches](../../protected_branches.md#require-code-owner-approval-on-a-protected-branch).
## Troubleshooting
### Approval rule name can't be blank
As a workaround for this validation error, you can delete the approval rule through
the API.
1. [GET a project-level rule](../../../../api/merge_request_approvals.md#get-a-single-project-level-rule).
1. [DELETE the rule](../../../../api/merge_request_approvals.md#delete-project-level-rule).
For more information about this validation error, read
[issue 285129](https://gitlab.com/gitlab-org/gitlab/-/issues/285129).

View File

@ -14,18 +14,40 @@ module Gitlab
end end
end end
def each_model_connection(models) def each_model_connection(models, &blk)
models.each do |model| models.each do |model|
connection_name = model.connection.pool.db_config.name # If model is shared, iterate all available base connections
# Example: `LooseForeignKeys::DeletedRecord`
with_shared_connection(model.connection, connection_name) do if model < ::Gitlab::Database::SharedModel
yield model, connection_name with_shared_model_connections(model, &blk)
else
with_model_connection(model, &blk)
end end
end end
end end
private private
def with_shared_model_connections(shared_model, &blk)
Gitlab::Database.database_base_models.each_pair do |connection_name, connection_model|
if shared_model.limit_connection_names
next unless shared_model.limit_connection_names.include?(connection_name.to_sym)
end
with_shared_connection(connection_model.connection, connection_name) do
yield shared_model, connection_name
end
end
end
def with_model_connection(model, &blk)
connection_name = model.connection.pool.db_config.name
with_shared_connection(model.connection, connection_name) do
yield model, connection_name
end
end
def with_shared_connection(connection, connection_name) def with_shared_connection(connection, connection_name)
Gitlab::Database::SharedModel.using_connection(connection) do Gitlab::Database::SharedModel.using_connection(connection) do
Gitlab::AppLogger.debug(message: 'Switched database connection', connection_name: connection_name) Gitlab::AppLogger.debug(message: 'Switched database connection', connection_name: connection_name)

View File

@ -3,19 +3,8 @@
module Gitlab module Gitlab
module Database module Database
module Partitioning module Partitioning
class TableWithoutModel class TableWithoutModel < Gitlab::Database::SharedModel
include PartitionedTable::ClassMethods include PartitionedTable
attr_reader :table_name
def initialize(table_name:, partitioned_column:, strategy:)
@table_name = table_name
partitioned_by(partitioned_column, strategy: strategy)
end
def connection
Gitlab::Database::SharedModel.connection
end
end end
class << self class << self
@ -77,7 +66,15 @@ module Gitlab
def registered_for_sync def registered_for_sync
registered_models + registered_tables.map do |table| registered_models + registered_tables.map do |table|
TableWithoutModel.new(**table) table_without_model(**table)
end
end
def table_without_model(table_name:, partitioned_column:, strategy:, limit_connection_names: nil)
Class.new(TableWithoutModel).tap do |klass|
klass.table_name = table_name
klass.partitioned_by(partitioned_column, strategy: strategy)
klass.limit_connection_names = limit_connection_names
end end
end end
end end

View File

@ -12,10 +12,15 @@ module Gitlab
def initialize(model) def initialize(model)
@model = model @model = model
@connection_name = model.connection.pool.db_config.name
end end
def sync_partitions def sync_partitions
Gitlab::AppLogger.info(message: "Checking state of dynamic postgres partitions", table_name: model.table_name) Gitlab::AppLogger.info(
message: "Checking state of dynamic postgres partitions",
table_name: model.table_name,
connection_name: @connection_name
)
# Double-checking before getting the lease: # Double-checking before getting the lease:
# The prevailing situation is no missing partitions and no extra partitions # The prevailing situation is no missing partitions and no extra partitions
@ -29,10 +34,13 @@ module Gitlab
detach(partitions_to_detach) unless partitions_to_detach.empty? detach(partitions_to_detach) unless partitions_to_detach.empty?
end end
rescue StandardError => e rescue StandardError => e
Gitlab::AppLogger.error(message: "Failed to create / detach partition(s)", Gitlab::AppLogger.error(
table_name: model.table_name, message: "Failed to create / detach partition(s)",
exception_class: e.class, table_name: model.table_name,
exception_message: e.message) exception_class: e.class,
exception_message: e.message,
connection_name: @connection_name
)
end end
private private
@ -98,9 +106,12 @@ module Gitlab
Postgresql::DetachedPartition.create!(table_name: partition.partition_name, Postgresql::DetachedPartition.create!(table_name: partition.partition_name,
drop_after: RETAIN_DETACHED_PARTITIONS_FOR.from_now) drop_after: RETAIN_DETACHED_PARTITIONS_FOR.from_now)
Gitlab::AppLogger.info(message: "Detached Partition", Gitlab::AppLogger.info(
partition_name: partition.partition_name, message: "Detached Partition",
table_name: partition.table) partition_name: partition.partition_name,
table_name: partition.table,
connection_name: @connection_name
)
end end
def assert_partition_detachable!(partition) def assert_partition_detachable!(partition)

View File

@ -6,6 +6,10 @@ module Gitlab
class SharedModel < ActiveRecord::Base class SharedModel < ActiveRecord::Base
self.abstract_class = true self.abstract_class = true
# if shared model is used, this allows to limit connections
# on which this model is being shared
class_attribute :limit_connection_names, default: nil
class << self class << self
def using_connection(connection) def using_connection(connection)
previous_connection = self.overriding_connection previous_connection = self.overriding_connection

View File

@ -10,10 +10,19 @@ module Gitlab
Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration) Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
# Returns [stdout + stderr, status] # Returns [stdout + stderr, status]
# status is either the exit code or the signal that killed the process
def popen(cmd, path = nil, vars = {}, &block) def popen(cmd, path = nil, vars = {}, &block)
result = popen_with_detail(cmd, path, vars, &block) result = popen_with_detail(cmd, path, vars, &block)
["#{result.stdout}#{result.stderr}", result.status&.exitstatus] # Process#waitpid returns Process::Status, which holds a 16-bit value.
# The higher-order 8 bits hold the exit() code (`exitstatus`).
# The lower-order bits holds whether the process was terminated.
# If the process didn't exit normally, `exitstatus` will be `nil`,
# but we still want a non-zero code, even if the value is
# platform-dependent.
status = result.status&.exitstatus || result.status.to_i
["#{result.stdout}#{result.stderr}", status]
end end
# Returns Result # Returns Result

View File

@ -236,7 +236,7 @@
redis_slot: code_review redis_slot: code_review
category: code_review category: code_review
aggregation: weekly aggregation: weekly
feature_flag: diff_searching_usage_data feature_flag: usage_data_diff_searches
- name: i_code_review_total_suggestions_applied - name: i_code_review_total_suggestions_applied
redis_slot: code_review redis_slot: code_review
category: code_review category: code_review

View File

@ -3,6 +3,7 @@
module LearnGitlab module LearnGitlab
class Project class Project
PROJECT_NAME = 'Learn GitLab' PROJECT_NAME = 'Learn GitLab'
PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial'
BOARD_NAME = 'GitLab onboarding' BOARD_NAME = 'GitLab onboarding'
LABEL_NAME = 'Novice' LABEL_NAME = 'Novice'
@ -15,7 +16,7 @@ module LearnGitlab
end end
def project def project
@project ||= current_user.projects.find_by_name(PROJECT_NAME) @project ||= current_user.projects.find_by_name([PROJECT_NAME, PROJECT_NAME_ULTIMATE_TRIAL])
end end
def board def board

View File

@ -28213,6 +28213,9 @@ msgstr ""
msgid "ProjectSettings|Merge requests approved for merge are queued, and pipelines validate the combined results of the source and target branches before merge. %{link_start}What are merge trains?%{link_end}" msgid "ProjectSettings|Merge requests approved for merge are queued, and pipelines validate the combined results of the source and target branches before merge. %{link_start}What are merge trains?%{link_end}"
msgstr "" msgstr ""
msgid "ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running."
msgstr ""
msgid "ProjectSettings|Merge suggestions" msgid "ProjectSettings|Merge suggestions"
msgstr "" msgstr ""
@ -28339,9 +28342,6 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin." msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr "" msgstr ""
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}"
msgstr ""
msgid "ProjectSettings|Transfer project" msgid "ProjectSettings|Transfer project"
msgstr "" msgstr ""

View File

@ -56,7 +56,7 @@
"@gitlab/at.js": "1.5.7", "@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0", "@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "2.2.0", "@gitlab/svgs": "2.2.0",
"@gitlab/ui": "34.0.0", "@gitlab/ui": "35.0.0",
"@gitlab/visual-review-tools": "1.6.1", "@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.4-1", "@rails/actioncable": "6.1.4-1",
"@rails/ujs": "6.1.4-1", "@rails/ujs": "6.1.4-1",

View File

@ -157,6 +157,7 @@ module QA
end end
def redirect_to_login_page(address) def redirect_to_login_page(address)
Menu.perform(&:sign_out_if_signed_in)
desired_host = URI(Runtime::Scenario.send("#{address}_address")).host desired_host = URI(Runtime::Scenario.send("#{address}_address")).host
Runtime::Browser.visit(address, Page::Main::Login) if desired_host != current_host Runtime::Browser.visit(address, Page::Main::Login) if desired_host != current_host
end end

View File

@ -83,10 +83,18 @@ module QA
element :merge_immediately_menu_item element :merge_immediately_menu_item
end end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do
element :head_mismatch_content
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do
element :squash_checkbox element :squash_checkbox
end end
view 'app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue' do
element :mr_widget_content
end
view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do
element :apply_suggestion_dropdown element :apply_suggestion_dropdown
element :commit_message_field element :commit_message_field
@ -269,13 +277,29 @@ module QA
has_element?(:merge_button, disabled: false) has_element?(:merge_button, disabled: false)
end end
# Waits up 60 seconds and raises an error if unable to merge # Waits up 60 seconds and raises an error if unable to merge.
def wait_until_ready_to_merge #
has_element?(:merge_button) # If a state is encountered in which a user would typically refresh the page, this will refresh the page and
# then check again if it's ready to merge. For example, it will refresh if a new change was pushed and the page
# needs to be refreshed to show the change.
#
# @param [Boolean] transient_test true if the current test is a transient test (default: false)
def wait_until_ready_to_merge(transient_test: false)
wait_until do
has_element?(:merge_button)
# The merge button is enabled via JS break true unless find_element(:merge_button).disabled?
wait_until(reload: false) do
!find_element(:merge_button).disabled? # If the widget shows "Merge blocked: new changes were just added" we can refresh the page and check again
next false if has_element?(:head_mismatch_content)
# Stop waiting if we're in a transient test. By this point we're in an unexpected state and should let the
# test fail so we can investigate. If we're not in a transient test we keep trying until we reach timeout.
next true unless transient_test
QA::Runtime::Logger.debug("MR widget text: #{mr_widget_text}")
false
end end
end end
@ -385,6 +409,10 @@ module QA
def cancel_auto_merge! def cancel_auto_merge!
click_element(:cancel_auto_merge_button) click_element(:cancel_auto_merge_button)
end end
def mr_widget_text
find_element(:mr_widget_content).text
end
end end
end end
end end

View File

@ -29,7 +29,8 @@ module QA
let!(:source_comment) { source_mr.add_comment('This is a test comment!') } let!(:source_comment) { source_mr.add_comment('This is a test comment!') }
let(:imported_mrs) { imported_project.merge_requests } let(:imported_mrs) { imported_project.merge_requests }
let(:imported_mr_comments) { imported_mr.comments } let(:imported_mr_comments) { imported_mr.comments.map { |note| note.except(:id, :noteable_id) } }
let(:source_mr_comments) { source_mr.comments.map { |note| note.except(:id, :noteable_id) } }
let(:imported_mr) do let(:imported_mr) do
Resource::MergeRequest.init do |mr| Resource::MergeRequest.init do |mr|
@ -53,8 +54,7 @@ module QA
aggregate_failures do aggregate_failures do
expect(imported_mr).to eq(source_mr.reload!) expect(imported_mr).to eq(source_mr.reload!)
expect(imported_mr_comments.count).to eq(1) expect(imported_mr_comments).to eq(source_mr_comments)
expect(imported_mr_comments.first.except(:id, :noteable_id)).to eq(source_comment.except(:id, :noteable_id))
end end
end end
end end

View File

@ -20,25 +20,6 @@ module QA
end end
before do before do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~EOF
test:
tags: ["runner-for-#{project.name}"]
script: sleep 20
only:
- merge_requests
EOF
}
]
)
end
Flow::Login.sign_in Flow::Login.sign_in
end end
@ -48,8 +29,10 @@ module QA
end end
it 'merges after pipeline succeeds' do it 'merges after pipeline succeeds' do
transient_test = repeat > 1
repeat.times do |i| repeat.times do |i|
QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if repeat > 1 QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if transient_test
branch_name = "mr-test-#{SecureRandom.hex(6)}-#{i}" branch_name = "mr-test-#{SecureRandom.hex(6)}-#{i}"
@ -68,19 +51,59 @@ module QA
merge_request.no_preparation = true merge_request.no_preparation = true
end end
# Load the page so that the browser is as prepared as possible to display the pipeline in progress when we
# start it.
merge_request.visit! merge_request.visit!
Page::MergeRequest::Show.perform do |mr| # Push a new pipeline config file
mr.merge_when_pipeline_succeeds! Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.branch = branch_name
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~EOF
test:
tags: ["runner-for-#{project.name}"]
script: sleep 20
only:
- merge_requests
EOF
}
]
)
end
Support::Waiter.wait_until(sleep_interval: 5) do Page::MergeRequest::Show.perform do |mr|
refresh
# Part of the challenge with this test is that the MR widget has many components that could be displayed
# and many errors states that those components could encounter. Most of the time few of those
# possible components will be relevant, so it would be inefficient for this test to check for each of
# them. Instead, we fail on anything but the expected state.
#
# The following method allows us to handle and ignore states (as we find them) that users could safely ignore.
mr.wait_until_ready_to_merge(transient_test: transient_test)
mr.retry_until(reload: true, message: 'Wait until ready to click MWPS') do
merge_request = merge_request.reload! merge_request = merge_request.reload!
merge_request.state == 'merged'
# Don't try to click MWPS if the MR is merged or the pipeline is complete
break if merge_request.state == 'merged' || project.pipelines.last[:status] == 'success'
# Try to click MWPS if this is a transient test, or if the MWPS button is visible,
# otherwise reload the page and retry
next false unless transient_test || mr.has_element?(:merge_button, text: 'Merge when pipeline succeeds')
# No need to keep retrying if we can click MWPS
break mr.merge_when_pipeline_succeeds!
end end
aggregate_failures do aggregate_failures do
expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
expect(mr.merged?).to be_truthy, "Expected content 'The changes were merged' but it did not appear." expect(mr.merged?).to be_truthy, "Expected content 'The changes were merged' but it did not appear."
expect(merge_request.reload!.merge_when_pipeline_succeeds).to be_truthy
end end
end end
end end

View File

@ -10,20 +10,20 @@ if [ $# -eq 0 ]; then
exit exit
fi fi
files=( $@ ) files=( "$@" )
len=${#files[@]} len=${#files[@]}
target=${files[$len-1]} target=${files[$len-1]}
# Trap interrupts and exit instead of continuing the loop # Trap interrupts and exit instead of continuing the loop
trap "echo Exited!; exit 2;" SIGINT SIGTERM trap "echo Exited!; exit 2;" SIGINT SIGTERM
# Show which set of specs are running # Show which set of specs are running and exit immediately if they fail.
set -x set -xe
# Do the speedy case first, run each spec with our failing spec # Do the speedy case first, run each spec with our failing spec
for file in "${files[@]}"; do for file in "${files[@]}"; do
bin/rspec $file $target bin/rspec "$file" "$target"
done done
# Do a full bisect given we did not find candidates with speedy cases # Do a full bisect given we did not find candidates with speedy cases
bin/rspec --bisect=verbose $@ bin/rspec --bisect=verbose "$@"

View File

@ -17,7 +17,6 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<gl-tabs-stub <gl-tabs-stub
contentclass="pt-0" contentclass="pt-0"
queryparamname="tab" queryparamname="tab"
theme="indigo"
value="0" value="0"
> >
<gl-tab-stub <gl-tab-stub

View File

@ -14,7 +14,6 @@ exports[`Code navigation popover component renders popover 1`] = `
contentclass="gl-py-0" contentclass="gl-py-0"
navclass="gl-hidden" navclass="gl-hidden"
queryparamname="tab" queryparamname="tab"
theme="indigo"
value="0" value="0"
> >
<gl-tab-stub <gl-tab-stub

View File

@ -40,7 +40,6 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
> >
<gl-tabs-stub <gl-tabs-stub
queryparamname="tab" queryparamname="tab"
theme="indigo"
value="0" value="0"
> >
<!----> <!---->

View File

@ -1,5 +1,5 @@
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceSelect, { import NamespaceSelect, {
i18n, i18n,
@ -7,6 +7,10 @@ import NamespaceSelect, {
} from '~/vue_shared/components/namespace_select/namespace_select.vue'; } from '~/vue_shared/components/namespace_select/namespace_select.vue';
import { user, group, namespaces } from './mock_data'; import { user, group, namespaces } from './mock_data';
const FLAT_NAMESPACES = [...group, ...user];
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
describe('Namespace Select', () => { describe('Namespace Select', () => {
let wrapper; let wrapper;
@ -16,67 +20,97 @@ describe('Namespace Select', () => {
data: namespaces, data: namespaces,
...props, ...props,
}, },
stubs: {
// We have to "full" mount GlDropdown so that slot children will render
GlDropdown,
},
}); });
const wrappersText = (arr) => arr.wrappers.map((w) => w.text()); const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
const flatNamespaces = () => [...group, ...user];
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownAttributes = (attr) => findDropdown().attributes(attr); const findDropdownText = () => findDropdown().props('text');
const selectedDropdownItemText = () => findDropdownAttributes('text');
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader); const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
beforeEach(() => { const search = (term) => findSearchBox().vm.$emit('input', term);
wrapper = createComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders the dropdown', () => { describe('default', () => {
expect(findDropdown().exists()).toBe(true); beforeEach(() => {
wrapper = createComponent();
});
it('renders the dropdown', () => {
expect(findDropdown().exists()).toBe(true);
});
it('renders each dropdown item', () => {
expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
});
it('renders default dropdown text', () => {
expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
});
it('splits group and user namespaces', () => {
const headers = findSectionHeaders();
expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
});
it('does not render wrapper as full width', () => {
expect(findDropdown().attributes('block')).toBeUndefined();
});
}); });
it('can override the default text', () => { it('with defaultText, it overrides dropdown text', () => {
const textOverride = 'Select an option'; const textOverride = 'Select an option';
wrapper = createComponent({ defaultText: textOverride }); wrapper = createComponent({ defaultText: textOverride });
expect(selectedDropdownItemText()).toBe(textOverride);
expect(findDropdownText()).toBe(textOverride);
}); });
it('renders each dropdown item', () => { it('with includeHeaders=false, hides group/user headers', () => {
const items = findDropdownItems().wrappers;
expect(items).toHaveLength(flatNamespaces().length);
});
it('renders the human name for each item', () => {
const dropdownItems = wrappersText(findDropdownItems());
const flatNames = flatNamespaces().map(({ humanName }) => humanName);
expect(dropdownItems).toEqual(flatNames);
});
it('sets the initial dropdown text', () => {
expect(selectedDropdownItemText()).toBe(i18n.DEFAULT_TEXT);
});
it('splits group and user namespaces', () => {
const headers = findSectionHeaders();
expect(headers).toHaveLength(2);
expect(wrappersText(headers)).toEqual([i18n.GROUPS, i18n.USERS]);
});
it('can hide the group / user headers', () => {
wrapper = createComponent({ includeHeaders: false }); wrapper = createComponent({ includeHeaders: false });
expect(findSectionHeaders()).toHaveLength(0); expect(findSectionHeaders()).toHaveLength(0);
}); });
it('sets the dropdown to full width', () => { it('with fullWidth=true, sets the dropdown to full width', () => {
expect(findDropdownAttributes('block')).toBeUndefined();
wrapper = createComponent({ fullWidth: true }); wrapper = createComponent({ fullWidth: true });
expect(findDropdownAttributes('block')).not.toBeUndefined(); expect(findDropdown().attributes('block')).toBe('true');
expect(findDropdownAttributes('block')).toBe('true'); });
describe('with search', () => {
it.each`
term | includeEmptyNamespace | expectedItems
${''} | ${false} | ${[...namespaces.group, ...namespaces.user]}
${'sub'} | ${false} | ${[namespaces.group[1]]}
${'User'} | ${false} | ${[...namespaces.user]}
${'User'} | ${true} | ${[...namespaces.user]}
${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...namespaces.user]}
`(
'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length',
async ({ term, includeEmptyNamespace, expectedItems }) => {
wrapper = createComponent({
includeEmptyNamespace,
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
});
search(term);
await nextTick();
const expected = expectedItems.map((x) => x.humanName);
expect(findDropdownItemsTexts()).toEqual(expected);
},
);
}); });
describe('with a selected namespace', () => { describe('with a selected namespace', () => {
@ -84,11 +118,13 @@ describe('Namespace Select', () => {
const selectedItem = group[selectedGroupIndex]; const selectedItem = group[selectedGroupIndex];
beforeEach(() => { beforeEach(() => {
wrapper = createComponent();
findDropdownItems().at(selectedGroupIndex).vm.$emit('click'); findDropdownItems().at(selectedGroupIndex).vm.$emit('click');
}); });
it('sets the dropdown text', () => { it('sets the dropdown text', () => {
expect(selectedDropdownItemText()).toBe(selectedItem.humanName); expect(findDropdownText()).toBe(selectedItem.humanName);
}); });
it('emits the `select` event when a namespace is selected', () => { it('emits the `select` event when a namespace is selected', () => {
@ -98,27 +134,35 @@ describe('Namespace Select', () => {
}); });
describe('with an empty namespace option', () => { describe('with an empty namespace option', () => {
const emptyNamespaceTitle = 'No namespace selected'; beforeEach(() => {
beforeEach(async () => {
wrapper = createComponent({ wrapper = createComponent({
includeEmptyNamespace: true, includeEmptyNamespace: true,
emptyNamespaceTitle, emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
}); });
await nextTick();
}); });
it('includes the empty namespace', () => { it('includes the empty namespace', () => {
const first = findDropdownItems().at(0); const first = findDropdownItems().at(0);
expect(first.text()).toBe(emptyNamespaceTitle);
expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
}); });
it('emits the `select` event when a namespace is selected', () => { it('emits the `select` event when a namespace is selected', () => {
findDropdownItems().at(0).vm.$emit('click'); findDropdownItems().at(0).vm.$emit('click');
expect(wrapper.emitted('select')).toEqual([ expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
[{ id: EMPTY_NAMESPACE_ID, humanName: emptyNamespaceTitle }], });
]);
it.each`
desc | term | shouldShow
${'should hide empty option'} | ${'group'} | ${false}
${'should show empty option'} | ${'Empty'} | ${true}
`('when search for $term, $desc', async ({ term, shouldShow }) => {
search(term);
await nextTick();
expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
}); });
}); });
}); });

View File

@ -4,45 +4,97 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::EachDatabase do RSpec.describe Gitlab::Database::EachDatabase do
describe '.each_database_connection' do describe '.each_database_connection' do
let(:expected_connections) do before do
Gitlab::Database.database_base_models.map { |name, model| [model.connection, name] } allow(Gitlab::Database).to receive(:database_base_models)
.and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
end end
it 'yields each connection after connecting SharedModel' do it 'yields each connection after connecting SharedModel', :add_ci_connection do
expected_connections.each do |connection, _| expect(Gitlab::Database::SharedModel).to receive(:using_connection)
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield .with(ActiveRecord::Base.connection).ordered.and_yield
end
yielded_connections = [] expect(Gitlab::Database::SharedModel).to receive(:using_connection)
.with(Ci::ApplicationRecord.connection).ordered.and_yield
described_class.each_database_connection do |connection, name| expect { |b| described_class.each_database_connection(&b) }
yielded_connections << [connection, name] .to yield_successive_args(
end [ActiveRecord::Base.connection, 'main'],
[Ci::ApplicationRecord.connection, 'ci']
expect(yielded_connections).to match_array(expected_connections) )
end end
end end
describe '.each_model_connection' do describe '.each_model_connection' do
let(:model1) { double(connection: double, table_name: 'table1') } context 'when the model inherits from SharedModel', :add_ci_connection do
let(:model2) { double(connection: double, table_name: 'table2') } let(:model1) { Class.new(Gitlab::Database::SharedModel) }
let(:model2) { Class.new(Gitlab::Database::SharedModel) }
before do before do
allow(model1.connection).to receive_message_chain('pool.db_config.name').and_return('name1') allow(Gitlab::Database).to receive(:database_base_models)
allow(model2.connection).to receive_message_chain('pool.db_config.name').and_return('name2') .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
end
it 'yields each model after connecting SharedModel' do
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model1.connection).and_yield
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model2.connection).and_yield
yielded_models = []
described_class.each_model_connection([model1, model2]) do |model, name|
yielded_models << [model, name]
end end
expect(yielded_models).to match_array([[model1, 'name1'], [model2, 'name2']]) it 'yields each model with SharedModel connected to each database connection' do
expect_yielded_models([model1, model2], [
{ model: model1, connection: ActiveRecord::Base.connection, name: 'main' },
{ model: model1, connection: Ci::ApplicationRecord.connection, name: 'ci' },
{ model: model2, connection: ActiveRecord::Base.connection, name: 'main' },
{ model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' }
])
end
context 'when the model limits connection names' do
before do
model1.limit_connection_names = %i[main]
model2.limit_connection_names = %i[ci]
end
it 'only yields the model with SharedModel connected to the limited connections' do
expect_yielded_models([model1, model2], [
{ model: model1, connection: ActiveRecord::Base.connection, name: 'main' },
{ model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' }
])
end
end
end
context 'when the model does not inherit from SharedModel' do
let(:main_model) { Class.new(ActiveRecord::Base) }
let(:ci_model) { Class.new(Ci::ApplicationRecord) }
let(:main_connection) { double(:connection) }
let(:ci_connection) { double(:connection) }
before do
allow(main_model).to receive(:connection).and_return(main_connection)
allow(ci_model).to receive(:connection).and_return(ci_connection)
allow(main_connection).to receive_message_chain('pool.db_config.name').and_return('main')
allow(ci_connection).to receive_message_chain('pool.db_config.name').and_return('ci')
end
it 'yields each model after connecting SharedModel' do
expect_yielded_models([main_model, ci_model], [
{ model: main_model, connection: main_connection, name: 'main' },
{ model: ci_model, connection: ci_connection, name: 'ci' }
])
end
end
def expect_yielded_models(models_to_iterate, expected_values)
times_yielded = 0
described_class.each_model_connection(models_to_iterate) do |model, name|
expected = expected_values[times_yielded]
expect(model).to be(expected[:model])
expect(model.connection).to be(expected[:connection])
expect(name).to eq(expected[:name])
times_yielded += 1
end
expect(times_yielded).to eq(expected_values.size)
end end
end end
end end

View File

@ -40,6 +40,17 @@ RSpec.describe Gitlab::Popen do
it { expect(@output).to include('No such file or directory') } it { expect(@output).to include('No such file or directory') }
end end
context 'non-zero status with a kill' do
let(:cmd) { [Gem.ruby, "-e", "thr = Thread.new { sleep 5 }; Process.kill(9, Process.pid); thr.join"] }
before do
@output, @status = @klass.new.popen(cmd)
end
it { expect(@status).to eq(9) }
it { expect(@output).to be_empty }
end
context 'unsafe string command' do context 'unsafe string command' do
it 'raises an error when it gets called with a string argument' do it 'raises an error when it gets called with a string argument' do
expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError) expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError)

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe LearnGitlab::Project do RSpec.describe LearnGitlab::Project do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) } let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME) }
let_it_be(:learn_gitlab_ultimate_trial_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL) }
let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) } let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: LearnGitlab::Project::BOARD_NAME) }
let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) } let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: LearnGitlab::Project::LABEL_NAME) }
@ -45,6 +46,12 @@ RSpec.describe LearnGitlab::Project do
subject { described_class.new(current_user).project } subject { described_class.new(current_user).project }
it { is_expected.to eq learn_gitlab_project } it { is_expected.to eq learn_gitlab_project }
context 'when it is created during trial signup' do
let_it_be(:learn_gitlab_project) { create(:project, name: LearnGitlab::Project::PROJECT_NAME_ULTIMATE_TRIAL) }
it { is_expected.to eq learn_gitlab_project }
end
end end
describe '.board' do describe '.board' do

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Delete a work item' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let(:current_user) { developer }
let(:mutation) { graphql_mutation(:workItemDelete, { 'id' => work_item.to_global_id.to_s }) }
let(:mutation_response) { graphql_mutation_response(:work_item_delete) }
context 'when the user is not allowed to delete a work item' do
let(:work_item) { create(:work_item, project: project) }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to delete a work item' do
let_it_be(:authored_work_item, refind: true) { create(:work_item, project: project, author: developer, assignees: [developer]) }
let(:work_item) { authored_work_item }
it 'deletes the work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(-1)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['project']).to include('id' => work_item.project.to_global_id.to_s)
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'does not delete the work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to not_change(WorkItem, :count)
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
end
end
end
end

View File

@ -27,6 +27,10 @@ RSpec.describe Pages::ZipDirectoryService do
let(:archive) { result[:archive_path] } let(:archive) { result[:archive_path] }
let(:entries_count) { result[:entries_count] } let(:entries_count) { result[:entries_count] }
it 'returns true if ZIP64 is enabled' do
expect(::Zip.write_zip64_support).to be true
end
shared_examples 'handles invalid public directory' do shared_examples 'handles invalid public directory' do
it 'returns success' do it 'returns success' do
expect(status).to eq(:success) expect(status).to eq(:success)
@ -35,7 +39,7 @@ RSpec.describe Pages::ZipDirectoryService do
end end
end end
context "when work direcotry doesn't exist" do context "when work directory doesn't exist" do
let(:service_directory) { "/tmp/not/existing/dir" } let(:service_directory) { "/tmp/not/existing/dir" }
include_examples 'handles invalid public directory' include_examples 'handles invalid public directory'

View File

@ -97,9 +97,13 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end end
end end
shared_examples_for "deleting a project with merge requests" do context "deleting a project with merge requests" do
let!(:merge_request) { create(:merge_request, source_project: project) } let!(:merge_request) { create(:merge_request, source_project: project) }
before do
allow(project).to receive(:destroy!).and_return(true)
end
it "deletes merge request and related records" do it "deletes merge request and related records" do
merge_request_diffs = merge_request.merge_request_diffs merge_request_diffs = merge_request.merge_request_diffs
expect(merge_request_diffs.size).to eq(1) expect(merge_request_diffs.size).to eq(1)
@ -119,25 +123,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
destroy_project(project, user, {}) destroy_project(project, user, {})
end end
context "extract_mr_diff_commit_deletions feature flag" do
context "with flag enabled" do
before do
stub_feature_flags(extract_mr_diff_commit_deletions: true)
allow(project).to receive(:destroy!).and_return(true)
end
it_behaves_like "deleting a project with merge requests"
end
context "with flag disabled" do
before do
stub_feature_flags(extract_mr_diff_commit_deletions: false)
end
it_behaves_like "deleting a project with merge requests"
end
end
context 'with running pipelines' do context 'with running pipelines' do
let!(:pipelines) { create_list(:ci_pipeline, 3, :running, project: project) } let!(:pipelines) { create_list(:ci_pipeline, 3, :running, project: project) }
let(:destroy_pipeline_service) { double('DestroyPipelineService', execute: nil) } let(:destroy_pipeline_service) { double('DestroyPipelineService', execute: nil) }

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::DeleteService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:guest) { create(:user) }
let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: guest) }
let(:user) { guest }
before_all do
project.add_guest(guest)
# note necessary to test note removal as part of work item deletion
create(:note, project: project, noteable: work_item)
end
describe '#execute' do
subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) }
context 'when user can delete the work item' do
it { is_expected.to be_success }
# currently we don't expect destroy to fail. Mocking here for coverage and keeping
# the service's return type consistent
context 'when there are errors preventing to delete the work item' do
before do
allow(work_item).to receive(:destroy).and_return(false)
work_item.errors.add(:title)
end
it { is_expected.to be_error }
it 'returns error messages' do
expect(result.errors).to contain_exactly('Title is invalid')
end
end
end
context 'when user cannot delete the work item' do
let(:user) { create(:user) }
it { is_expected.to be_error }
it 'returns error messages' do
expect(result.errors).to contain_exactly('User not authorized to delete work item')
end
end
end
end

View File

@ -962,10 +962,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.2.0.tgz#95cf58d6ae634d535145159f08f5cff6241d4013" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.2.0.tgz#95cf58d6ae634d535145159f08f5cff6241d4013"
integrity sha512-mCwR3KfNPsxRoojtTjMIZwdd4FFlBh5DlR9AeodP+7+k8rILdWGYxTZbJMPNXoPbZx16R94nG8c5bR7toD4QBw== integrity sha512-mCwR3KfNPsxRoojtTjMIZwdd4FFlBh5DlR9AeodP+7+k8rILdWGYxTZbJMPNXoPbZx16R94nG8c5bR7toD4QBw==
"@gitlab/ui@34.0.0": "@gitlab/ui@35.0.0":
version "34.0.0" version "35.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-34.0.0.tgz#0fe9574df2c38aeb63add94e4549ed4e65975ef8" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-35.0.0.tgz#9fb89babddc337830f1245044fe7946b266395b4"
integrity sha512-BFh3x+GCqWAoWhNJhJUunW3eHQLQkBOTBwZFJWSS+1+9ZtetqU3t0/OoqYjJuyTsqdra7A/e6BZsU0j7CnbY+Q== integrity sha512-iGGsLFgy/BOnmym2VBT+ByiP7mY/DtJPDSoYjd7QtJbOF17A+MyvOwBFGTUXAJxDtWTYSkMZkEuwZVA3VOEwyQ==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
bootstrap-vue "2.20.1" bootstrap-vue "2.20.1"