Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3bdc719293
commit
143a33345c
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,9 +43,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectedNamespaceId() {
|
|
||||||
return this.selectedId;
|
|
||||||
},
|
|
||||||
disableSubmitButton() {
|
disableSubmitButton() {
|
||||||
return this.isPaidGroup || !this.selectedId;
|
return this.isPaidGroup || !this.selectedId;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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
|
|
|
@ -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'
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# Zip64 is needed to support archives with more than 65535 entries.
|
||||||
|
Zip.write_zip64_support = true
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)**
|
||||||
|
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 "$@"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
<!---->
|
<!---->
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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'
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue