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

View File

@ -43,9 +43,6 @@ export default {
};
},
computed: {
selectedNamespaceId() {
return this.selectedId;
},
disableSubmitButton() {
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';
const prepareGroups = (rawGroups) => {
if (!rawGroups) {
return { group: [] };
}
const group = JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
id,
humanName,
@ -22,7 +26,7 @@ export default () => {
targetFormId = null,
buttonText: confirmButtonText = '',
groupName = '',
parentGroups = [],
parentGroups,
isPaidGroup,
} = el.dataset;

View File

@ -578,7 +578,7 @@ export default {
: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" />
<ready-to-merge
v-if="isRestructuredMrWidgetEnabled && mr.commitsCount"

View File

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

View File

@ -47,7 +47,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Usage data feature flags
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_searching_usage_data, @project, default_enabled: :yaml)
push_frontend_feature_flag(:usage_data_diff_searches, @project, default_enabled: :yaml)
end
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::Echo
mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items
mount_mutation Mutations::WorkItems::Delete
mount_mutation Mutations::WorkItems::Update
end
end

View File

@ -2,4 +2,11 @@
class WorkItemPolicy < BasePolicy
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

View File

@ -139,10 +139,7 @@ module Projects
destroy_web_hooks!
destroy_project_bots!
destroy_ci_records!
if ::Feature.enabled?(:extract_mr_diff_commit_deletions, default_enabled: :yaml)
destroy_mr_diff_commits!
end
destroy_mr_diff_commits!
# Rails attempts to load all related records into memory before
# 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
= s_('ProjectSettings|Pipelines must succeed')
.text-secondary
- configuring_pipelines_for_merge_requests_help_link_url = help_page_path('ci/pipelines/merge_request_pipelines.md', anchor: 'prerequisites')
- 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 }
= s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
.form-check.mb-2
.gl-pl-6
= form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input'

View File

@ -6,36 +6,6 @@
- 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")
- 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 note.emoji_awardable?
.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:
rollout_issue_url:
milestone: '14.2'

View File

@ -15,10 +15,12 @@ if Gitlab.ee?
else
Gitlab::Database::Partitioning.register_tables([
{
limit_connection_names: %i[main],
table_name: 'incident_management_pending_alert_escalations',
partitioned_column: :process_at, strategy: :monthly
},
{
limit_connection_names: %i[main],
table_name: 'incident_management_pending_issue_escalations',
partitioned_column: :process_at, strategy: :monthly
}
@ -31,6 +33,7 @@ unless Gitlab.jh?
# This should be synchronized with the following model:
# 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',
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 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> |
| 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> |
| 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> |
@ -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 |
| 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 |
| 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 |
| 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:
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="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`
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
[Service Ping](index.md) metrics are defined in the
[Metrics Dictionary](https://metrics.gitlab.com/).
[Service Ping](index.md) metrics are defined in individual YAML files definitions from which the
[Metrics Dictionary](https://metrics.gitlab.com/) is built.
This guide describes the dictionary and how it's implemented.
## Metrics Definition and validation
@ -194,7 +194,7 @@ tier:
- 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.
@ -229,7 +229,7 @@ bundle exec rails generate gitlab:usage_metric_definition counts.issues --ee --d
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.
@ -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_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
[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
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
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
@ -309,7 +309,11 @@ locked. Projects can only be unlocked by purchasing more storage subscription un
### 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

View File

@ -30,8 +30,8 @@ to Kubernetes clusters using the [GitLab Agent](../user/clusters/agent/install/i
#### GitOps deployments **(PREMIUM)**
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
With the [GitLab Agent](../user/clusters/agent/install/index.md), you can perform [pull-based
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.
#### 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
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)**

View File

@ -229,3 +229,16 @@ approval rule for certain branches:
![Scoped to protected branch](img/scoped_to_protected_branch_v13_10.png)
1. To enable this configuration, read
[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
def each_model_connection(models)
def each_model_connection(models, &blk)
models.each do |model|
connection_name = model.connection.pool.db_config.name
with_shared_connection(model.connection, connection_name) do
yield model, connection_name
# If model is shared, iterate all available base connections
# Example: `LooseForeignKeys::DeletedRecord`
if model < ::Gitlab::Database::SharedModel
with_shared_model_connections(model, &blk)
else
with_model_connection(model, &blk)
end
end
end
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)
Gitlab::Database::SharedModel.using_connection(connection) do
Gitlab::AppLogger.debug(message: 'Switched database connection', connection_name: connection_name)

View File

@ -3,19 +3,8 @@
module Gitlab
module Database
module Partitioning
class TableWithoutModel
include PartitionedTable::ClassMethods
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
class TableWithoutModel < Gitlab::Database::SharedModel
include PartitionedTable
end
class << self
@ -77,7 +66,15 @@ module Gitlab
def registered_for_sync
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

View File

@ -12,10 +12,15 @@ module Gitlab
def initialize(model)
@model = model
@connection_name = model.connection.pool.db_config.name
end
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:
# 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?
end
rescue StandardError => e
Gitlab::AppLogger.error(message: "Failed to create / detach partition(s)",
table_name: model.table_name,
exception_class: e.class,
exception_message: e.message)
Gitlab::AppLogger.error(
message: "Failed to create / detach partition(s)",
table_name: model.table_name,
exception_class: e.class,
exception_message: e.message,
connection_name: @connection_name
)
end
private
@ -98,9 +106,12 @@ module Gitlab
Postgresql::DetachedPartition.create!(table_name: partition.partition_name,
drop_after: RETAIN_DETACHED_PARTITIONS_FOR.from_now)
Gitlab::AppLogger.info(message: "Detached Partition",
partition_name: partition.partition_name,
table_name: partition.table)
Gitlab::AppLogger.info(
message: "Detached Partition",
partition_name: partition.partition_name,
table_name: partition.table,
connection_name: @connection_name
)
end
def assert_partition_detachable!(partition)

View File

@ -6,6 +6,10 @@ module Gitlab
class SharedModel < ActiveRecord::Base
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
def using_connection(connection)
previous_connection = self.overriding_connection

View File

@ -10,10 +10,19 @@ module Gitlab
Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
# Returns [stdout + stderr, status]
# status is either the exit code or the signal that killed the process
def popen(cmd, path = nil, 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
# Returns Result

View File

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

View File

@ -3,6 +3,7 @@
module LearnGitlab
class Project
PROJECT_NAME = 'Learn GitLab'
PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial'
BOARD_NAME = 'GitLab onboarding'
LABEL_NAME = 'Novice'
@ -15,7 +16,7 @@ module LearnGitlab
end
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
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}"
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"
msgstr ""
@ -28339,9 +28342,6 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}"
msgstr ""
msgid "ProjectSettings|Transfer project"
msgstr ""

View File

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

View File

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

View File

@ -83,10 +83,18 @@ module QA
element :merge_immediately_menu_item
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
element :squash_checkbox
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
element :apply_suggestion_dropdown
element :commit_message_field
@ -269,13 +277,29 @@ module QA
has_element?(:merge_button, disabled: false)
end
# Waits up 60 seconds and raises an error if unable to merge
def wait_until_ready_to_merge
has_element?(:merge_button)
# Waits up 60 seconds and raises an error if unable to merge.
#
# 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
wait_until(reload: false) do
!find_element(:merge_button).disabled?
break true unless 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
@ -385,6 +409,10 @@ module QA
def cancel_auto_merge!
click_element(:cancel_auto_merge_button)
end
def mr_widget_text
find_element(:mr_widget_content).text
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(: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
Resource::MergeRequest.init do |mr|
@ -53,8 +54,7 @@ module QA
aggregate_failures do
expect(imported_mr).to eq(source_mr.reload!)
expect(imported_mr_comments.count).to eq(1)
expect(imported_mr_comments.first.except(:id, :noteable_id)).to eq(source_comment.except(:id, :noteable_id))
expect(imported_mr_comments).to eq(source_mr_comments)
end
end
end

View File

@ -20,25 +20,6 @@ module QA
end
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
end
@ -48,8 +29,10 @@ module QA
end
it 'merges after pipeline succeeds' do
transient_test = repeat > 1
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}"
@ -68,19 +51,59 @@ module QA
merge_request.no_preparation = true
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!
Page::MergeRequest::Show.perform do |mr|
mr.merge_when_pipeline_succeeds!
# Push a new pipeline config file
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.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
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(merge_request.reload!.merge_when_pipeline_succeeds).to be_truthy
end
end
end

View File

@ -10,20 +10,20 @@ if [ $# -eq 0 ]; then
exit
fi
files=( $@ )
files=( "$@" )
len=${#files[@]}
target=${files[$len-1]}
# Trap interrupts and exit instead of continuing the loop
trap "echo Exited!; exit 2;" SIGINT SIGTERM
# Show which set of specs are running
set -x
# Show which set of specs are running and exit immediately if they fail.
set -xe
# Do the speedy case first, run each spec with our failing spec
for file in "${files[@]}"; do
bin/rspec $file $target
bin/rspec "$file" "$target"
done
# 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
contentclass="pt-0"
queryparamname="tab"
theme="indigo"
value="0"
>
<gl-tab-stub

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 NamespaceSelect, {
i18n,
@ -7,6 +7,10 @@ import NamespaceSelect, {
} from '~/vue_shared/components/namespace_select/namespace_select.vue';
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', () => {
let wrapper;
@ -16,67 +20,97 @@ describe('Namespace Select', () => {
data: namespaces,
...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 flatNamespaces = () => [...group, ...user];
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownAttributes = (attr) => findDropdown().attributes(attr);
const selectedDropdownItemText = () => findDropdownAttributes('text');
const findDropdownText = () => findDropdown().props('text');
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
beforeEach(() => {
wrapper = createComponent();
});
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const search = (term) => findSearchBox().vm.$emit('input', term);
afterEach(() => {
wrapper.destroy();
});
it('renders the dropdown', () => {
expect(findDropdown().exists()).toBe(true);
describe('default', () => {
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';
wrapper = createComponent({ defaultText: textOverride });
expect(selectedDropdownItemText()).toBe(textOverride);
expect(findDropdownText()).toBe(textOverride);
});
it('renders each dropdown item', () => {
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', () => {
it('with includeHeaders=false, hides group/user headers', () => {
wrapper = createComponent({ includeHeaders: false });
expect(findSectionHeaders()).toHaveLength(0);
});
it('sets the dropdown to full width', () => {
expect(findDropdownAttributes('block')).toBeUndefined();
it('with fullWidth=true, sets the dropdown to full width', () => {
wrapper = createComponent({ fullWidth: true });
expect(findDropdownAttributes('block')).not.toBeUndefined();
expect(findDropdownAttributes('block')).toBe('true');
expect(findDropdown().attributes('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', () => {
@ -84,11 +118,13 @@ describe('Namespace Select', () => {
const selectedItem = group[selectedGroupIndex];
beforeEach(() => {
wrapper = createComponent();
findDropdownItems().at(selectedGroupIndex).vm.$emit('click');
});
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', () => {
@ -98,27 +134,35 @@ describe('Namespace Select', () => {
});
describe('with an empty namespace option', () => {
const emptyNamespaceTitle = 'No namespace selected';
beforeEach(async () => {
beforeEach(() => {
wrapper = createComponent({
includeEmptyNamespace: true,
emptyNamespaceTitle,
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
});
await nextTick();
});
it('includes the empty namespace', () => {
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', () => {
findDropdownItems().at(0).vm.$emit('click');
expect(wrapper.emitted('select')).toEqual([
[{ id: EMPTY_NAMESPACE_ID, humanName: emptyNamespaceTitle }],
]);
expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
});
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
describe '.each_database_connection' do
let(:expected_connections) do
Gitlab::Database.database_base_models.map { |name, model| [model.connection, name] }
before do
allow(Gitlab::Database).to receive(:database_base_models)
.and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
end
it 'yields each connection after connecting SharedModel' do
expected_connections.each do |connection, _|
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield
end
it 'yields each connection after connecting SharedModel', :add_ci_connection do
expect(Gitlab::Database::SharedModel).to receive(:using_connection)
.with(ActiveRecord::Base.connection).ordered.and_yield
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|
yielded_connections << [connection, name]
end
expect(yielded_connections).to match_array(expected_connections)
expect { |b| described_class.each_database_connection(&b) }
.to yield_successive_args(
[ActiveRecord::Base.connection, 'main'],
[Ci::ApplicationRecord.connection, 'ci']
)
end
end
describe '.each_model_connection' do
let(:model1) { double(connection: double, table_name: 'table1') }
let(:model2) { double(connection: double, table_name: 'table2') }
context 'when the model inherits from SharedModel', :add_ci_connection do
let(:model1) { Class.new(Gitlab::Database::SharedModel) }
let(:model2) { Class.new(Gitlab::Database::SharedModel) }
before do
allow(model1.connection).to receive_message_chain('pool.db_config.name').and_return('name1')
allow(model2.connection).to receive_message_chain('pool.db_config.name').and_return('name2')
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]
before do
allow(Gitlab::Database).to receive(:database_base_models)
.and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access)
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

View File

@ -40,6 +40,17 @@ RSpec.describe Gitlab::Popen do
it { expect(@output).to include('No such file or directory') }
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
it 'raises an error when it gets called with a string argument' do
expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError)

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe LearnGitlab::Project do
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_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_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 }
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
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(: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
it 'returns success' do
expect(status).to eq(:success)
@ -35,7 +39,7 @@ RSpec.describe Pages::ZipDirectoryService do
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" }
include_examples 'handles invalid public directory'

View File

@ -97,9 +97,13 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
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) }
before do
allow(project).to receive(:destroy!).and_return(true)
end
it "deletes merge request and related records" do
merge_request_diffs = merge_request.merge_request_diffs
expect(merge_request_diffs.size).to eq(1)
@ -119,25 +123,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
destroy_project(project, user, {})
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
let!(:pipelines) { create_list(:ci_pipeline, 3, :running, project: project) }
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"
integrity sha512-mCwR3KfNPsxRoojtTjMIZwdd4FFlBh5DlR9AeodP+7+k8rILdWGYxTZbJMPNXoPbZx16R94nG8c5bR7toD4QBw==
"@gitlab/ui@34.0.0":
version "34.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-34.0.0.tgz#0fe9574df2c38aeb63add94e4549ed4e65975ef8"
integrity sha512-BFh3x+GCqWAoWhNJhJUunW3eHQLQkBOTBwZFJWSS+1+9ZtetqU3t0/OoqYjJuyTsqdra7A/e6BZsU0j7CnbY+Q==
"@gitlab/ui@35.0.0":
version "35.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-35.0.0.tgz#9fb89babddc337830f1245044fe7946b266395b4"
integrity sha512-iGGsLFgy/BOnmym2VBT+ByiP7mY/DtJPDSoYjd7QtJbOF17A+MyvOwBFGTUXAJxDtWTYSkMZkEuwZVA3VOEwyQ==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "2.20.1"