Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1613500bf7
commit
a373ecffca
|
|
@ -94,13 +94,13 @@ review-deploy:
|
|||
before_script:
|
||||
- *base-before_script
|
||||
|
||||
review-stop-failed-deployment:
|
||||
review-delete-deployment:
|
||||
extends:
|
||||
- .review-stop-base
|
||||
- .review:rules:review-stop-failed-deployment
|
||||
- .review:rules:review-delete-deployment
|
||||
stage: prepare
|
||||
script:
|
||||
- delete_failed_release
|
||||
- delete_release
|
||||
|
||||
review-stop:
|
||||
extends:
|
||||
|
|
@ -108,7 +108,7 @@ review-stop:
|
|||
- .review:rules:review-stop
|
||||
stage: post-qa
|
||||
script:
|
||||
- delete_release
|
||||
- delete_k8s_release_namespace
|
||||
|
||||
.review-qa-base:
|
||||
extends:
|
||||
|
|
|
|||
|
|
@ -1180,7 +1180,7 @@
|
|||
- <<: *if-dot-com-gitlab-org-schedule
|
||||
allow_failure: true
|
||||
|
||||
.review:rules:review-stop-failed-deployment:
|
||||
.review:rules:review-delete-deployment:
|
||||
rules:
|
||||
- <<: *if-not-ee
|
||||
when: never
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
fef798978197809e268e5395838b4f4eeb4288e9
|
||||
49893735ed64feebc208f4efe90bebbb8bbb02ad
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -394,7 +394,7 @@ group :development, :test do
|
|||
end
|
||||
|
||||
group :development, :test, :danger do
|
||||
gem 'gitlab-dangerfiles', '~> 2.1.2', require: false
|
||||
gem 'gitlab-dangerfiles', '~> 2.2.1', require: false
|
||||
end
|
||||
|
||||
group :development, :test, :coverage do
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ GEM
|
|||
css_parser (1.7.0)
|
||||
addressable
|
||||
daemons (1.3.1)
|
||||
danger (8.2.3)
|
||||
danger (8.3.1)
|
||||
claide (~> 1.0)
|
||||
claide-plugins (>= 0.9.2)
|
||||
colored2 (~> 3.1)
|
||||
|
|
@ -468,8 +468,9 @@ GEM
|
|||
terminal-table (~> 1.5, >= 1.5.1)
|
||||
gitlab-chronic (0.10.5)
|
||||
numerizer (~> 0.2)
|
||||
gitlab-dangerfiles (2.1.2)
|
||||
danger-gitlab
|
||||
gitlab-dangerfiles (2.2.1)
|
||||
danger (>= 8.3.1)
|
||||
danger-gitlab (>= 8.0.0)
|
||||
gitlab-experiment (0.6.1)
|
||||
activesupport (>= 3.0)
|
||||
request_store (>= 1.0)
|
||||
|
|
@ -1488,7 +1489,7 @@ DEPENDENCIES
|
|||
gitaly (~> 14.1.0.pre.rc2)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-dangerfiles (~> 2.1.2)
|
||||
gitlab-dangerfiles (~> 2.2.1)
|
||||
gitlab-experiment (~> 0.6.1)
|
||||
gitlab-fog-azure-rm (~> 1.1.1)
|
||||
gitlab-labkit (~> 0.18.0)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default {
|
|||
<slot name="filepath-prepend"></slot>
|
||||
|
||||
<template v-if="blob.path">
|
||||
<file-icon :file-name="blob.path" :size="18" aria-hidden="true" css-classes="mr-2" />
|
||||
<file-icon :file-name="blob.path" :size="16" aria-hidden="true" css-classes="mr-2" />
|
||||
<strong
|
||||
class="file-title-name mr-1 js-blob-header-filepath"
|
||||
data-qa-selector="file_title_content"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import CommitPipelinesTable from './pipelines_table.vue';
|
||||
|
||||
/**
|
||||
* Used in:
|
||||
|
|
@ -23,12 +22,15 @@ export default () => {
|
|||
|
||||
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
|
||||
const table = new Vue({
|
||||
components: {
|
||||
CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
|
||||
},
|
||||
provide: {
|
||||
artifactsEndpoint: pipelineTableViewEl.dataset.artifactsEndpoint,
|
||||
artifactsEndpointPlaceholder: pipelineTableViewEl.dataset.artifactsEndpointPlaceholder,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(CommitPipelinesTable, {
|
||||
return createElement('commit-pipelines-table', {
|
||||
props: {
|
||||
endpoint: pipelineTableViewEl.dataset.endpoint,
|
||||
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export default {
|
|||
>
|
||||
<file-icon
|
||||
:file-name="filePath"
|
||||
:size="18"
|
||||
:size="16"
|
||||
aria-hidden="true"
|
||||
css-classes="gl-mr-2"
|
||||
:submodule="diffFile.submodule"
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default {
|
|||
<div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
|
||||
<gl-icon :name="collapseIcon" :size="16" class="gl-mr-2" />
|
||||
</div>
|
||||
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
|
||||
<file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-2" />
|
||||
<strong
|
||||
v-gl-tooltip
|
||||
:title="filePath"
|
||||
|
|
|
|||
|
|
@ -9,19 +9,23 @@ export default class IssuableContext {
|
|||
this.userSelect = new UsersSelect(currentUser);
|
||||
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
|
||||
|
||||
import(/* webpackChunkName: 'select2' */ 'select2/select2')
|
||||
.then(() => {
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
loadCSSFile(gon.select2_css_path)
|
||||
.then(() => {
|
||||
$('select.select2').select2({
|
||||
width: 'resolve',
|
||||
dropdownAutoWidth: true,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
const $select2 = $('select.select2');
|
||||
|
||||
if ($select2.length) {
|
||||
import(/* webpackChunkName: 'select2' */ 'select2/select2')
|
||||
.then(() => {
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
loadCSSFile(gon.select2_css_path)
|
||||
.then(() => {
|
||||
$select2.select2({
|
||||
width: 'resolve',
|
||||
dropdownAutoWidth: true,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
|
||||
return $(this).submit();
|
||||
|
|
|
|||
|
|
@ -153,9 +153,9 @@ export default {
|
|||
</template>
|
||||
</issuable-discussion>
|
||||
|
||||
<issuable-sidebar @sidebar-toggle="$emit('sidebar-toggle', $event)">
|
||||
<template #right-sidebar-items="sidebarProps">
|
||||
<slot name="right-sidebar-items" v-bind="sidebarProps"></slot>
|
||||
<issuable-sidebar>
|
||||
<template #right-sidebar-items="{ sidebarExpanded, toggleSidebar }">
|
||||
<slot name="right-sidebar-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot>
|
||||
</template>
|
||||
</issuable-sidebar>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { USER_COLLAPSED_GUTTER_COOKIE } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
data() {
|
||||
const userExpanded = !parseBoolean(Cookies.get('collapsed_gutter'));
|
||||
const userExpanded = !parseBoolean(Cookies.get(USER_COLLAPSED_GUTTER_COOKIE));
|
||||
|
||||
// We're deliberately keeping two different props for sidebar status;
|
||||
// 1. userExpanded reflects value based on cookie `collapsed_gutter`.
|
||||
|
|
@ -20,13 +20,6 @@ export default {
|
|||
isExpanded: userExpanded ? bp.isDesktop() : userExpanded,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isExpanded(expanded) {
|
||||
this.$emit('sidebar-toggle', {
|
||||
expanded,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
this.updatePageContainerClass();
|
||||
|
|
@ -49,11 +42,11 @@ export default {
|
|||
this.updatePageContainerClass();
|
||||
}
|
||||
},
|
||||
handleToggleSidebarClick() {
|
||||
toggleSidebar() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.userExpanded = this.isExpanded;
|
||||
|
||||
Cookies.set('collapsed_gutter', !this.userExpanded);
|
||||
Cookies.set(USER_COLLAPSED_GUTTER_COOKIE, !this.userExpanded);
|
||||
this.updatePageContainerClass();
|
||||
},
|
||||
},
|
||||
|
|
@ -68,8 +61,9 @@ export default {
|
|||
>
|
||||
<button
|
||||
class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!"
|
||||
data-testid="toggle-right-sidebar-button"
|
||||
:title="__('Toggle sidebar')"
|
||||
@click="handleToggleSidebarClick"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{
|
||||
__('Collapse sidebar')
|
||||
|
|
@ -83,7 +77,10 @@ export default {
|
|||
/>
|
||||
</button>
|
||||
<div data-testid="sidebar-items" class="issuable-sidebar">
|
||||
<slot name="right-sidebar-items" v-bind="{ sidebarExpanded: isExpanded }"></slot>
|
||||
<slot
|
||||
name="right-sidebar-items"
|
||||
v-bind="{ sidebarExpanded: isExpanded, toggleSidebar }"
|
||||
></slot>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const USER_COLLAPSED_GUTTER_COOKIE = 'collapsed_gutter';
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
mutation updateIssue($input: UpdateIssueInput!) {
|
||||
updateIssue(input: $input) {
|
||||
issuable: issue {
|
||||
id
|
||||
state
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export default {
|
|||
>
|
||||
<div class="js-file-title file-title file-title-flex-parent cursor-default">
|
||||
<div class="file-header-content" data-testid="file-name">
|
||||
<file-icon :file-name="file.filePath" :size="18" css-classes="gl-mr-2" />
|
||||
<file-icon :file-name="file.filePath" :size="16" css-classes="gl-mr-2" />
|
||||
<strong class="file-title-name">{{ file.filePath }}</strong>
|
||||
</div>
|
||||
<div class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start">
|
||||
|
|
|
|||
|
|
@ -148,14 +148,6 @@ MergeRequest.prototype.initCommitMessageListeners = function () {
|
|||
});
|
||||
};
|
||||
|
||||
MergeRequest.setStatusBoxToMerged = function () {
|
||||
$('.detail-page-header .status-box')
|
||||
.removeClass('status-box-open')
|
||||
.addClass('status-box-mr-merged')
|
||||
.find('span')
|
||||
.text(__('Merged'));
|
||||
};
|
||||
|
||||
MergeRequest.decreaseCounter = function (by = 1) {
|
||||
const $el = $('.js-merge-counter');
|
||||
const count = Math.max(parseInt($el.text().replace(/[^\d]/, ''), 10) - by, 0);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
|||
import $ from 'jquery';
|
||||
import Cookies from 'js-cookie';
|
||||
import Vue from 'vue';
|
||||
import CommitPipelinesTable from '~/commit/pipelines/pipelines_table.vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import initAddContextCommitsTriggers from './add_context_commits_modal';
|
||||
import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
||||
import Diff from './diff';
|
||||
import createFlash from './flash';
|
||||
|
|
@ -341,8 +339,10 @@ export default class MergeRequestTabs {
|
|||
this.scrollToContainerElement('#commits');
|
||||
|
||||
this.toggleLoading(false);
|
||||
initAddContextCommitsTriggers();
|
||||
|
||||
return import('./add_context_commits_modal');
|
||||
})
|
||||
.then((m) => m.default())
|
||||
.catch(() => {
|
||||
this.toggleLoading(false);
|
||||
createFlash({
|
||||
|
|
@ -356,13 +356,16 @@ export default class MergeRequestTabs {
|
|||
const { mrWidgetData } = gl;
|
||||
|
||||
this.commitPipelinesTable = new Vue({
|
||||
components: {
|
||||
CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
|
||||
},
|
||||
provide: {
|
||||
artifactsEndpoint: pipelineTableViewEl.dataset.artifactsEndpoint,
|
||||
artifactsEndpointPlaceholder: pipelineTableViewEl.dataset.artifactsEndpointPlaceholder,
|
||||
targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(CommitPipelinesTable, {
|
||||
return createElement('commit-pipelines-table', {
|
||||
props: {
|
||||
endpoint: pipelineTableViewEl.dataset.endpoint,
|
||||
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const CONTAINER_SCANNING_CONFIG_HELP_PATH = helpPagePath(
|
|||
{ anchor: 'configuration' },
|
||||
);
|
||||
|
||||
export const CLUSTER_IMAGE_SCANNING_NAME = __('ciReport|Cluster Image Scanning');
|
||||
export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning');
|
||||
export const CLUSTER_IMAGE_SCANNING_DESCRIPTION = __(
|
||||
'Check your Kubernetes cluster images for known vulnerabilities.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mutation($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
|
||||
updateIssue(input: { projectPath: $projectPath, iid: $iid, healthStatus: $healthStatus }) {
|
||||
issue {
|
||||
issuable: issue {
|
||||
id
|
||||
healthStatus
|
||||
}
|
||||
errors
|
||||
|
|
|
|||
|
|
@ -412,7 +412,6 @@ export default {
|
|||
// If state is merged we should update the widget and stop the polling
|
||||
eventHub.$emit('MRWidgetUpdateRequested');
|
||||
eventHub.$emit('FetchActionsContent');
|
||||
MergeRequest.setStatusBoxToMerged();
|
||||
MergeRequest.hideCloseButton();
|
||||
MergeRequest.decreaseCounter();
|
||||
stopPolling();
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ export default {
|
|||
:name="folderIconName"
|
||||
:size="size"
|
||||
class="folder-icon"
|
||||
use-deprecated-sizes
|
||||
data-qa-selector="folder_icon_content"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ class SearchController < ApplicationController
|
|||
search_term_present && !params[:project_id].present?
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
|
||||
|
||||
layout 'search'
|
||||
|
||||
feature_category :global_search
|
||||
|
|
@ -150,6 +152,15 @@ class SearchController < ApplicationController
|
|||
|
||||
redirect_to new_user_session_path, alert: _('You must be logged in to search across all of GitLab')
|
||||
end
|
||||
|
||||
def render_timeout(exception)
|
||||
raise exception unless action_name.to_sym == :show
|
||||
|
||||
log_exception(exception)
|
||||
|
||||
@timeout = true
|
||||
render status: :request_timeout
|
||||
end
|
||||
end
|
||||
|
||||
SearchController.prepend_mod_with('SearchController')
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ module MergedAtFilter
|
|||
mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
|
||||
mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
|
||||
|
||||
join_metrics(items, mr_metrics_scope)
|
||||
items.join_metrics.merge(mr_metrics_scope)
|
||||
end
|
||||
|
||||
def merged_after
|
||||
|
|
@ -20,22 +20,4 @@ module MergedAtFilter
|
|||
def merged_before
|
||||
params[:merged_before]
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
#
|
||||
# This join optimizes merged_at queries when the finder is invoked for a project by moving
|
||||
# the target_project_id condition from merge_requests table to merge_request_metrics table.
|
||||
def join_metrics(items, mr_metrics_scope)
|
||||
scope = if project_id = items.where_values_hash["target_project_id"]
|
||||
# removing the original merge_requests.target_project_id condition
|
||||
items = items.unscope(where: :target_project_id)
|
||||
# adding the target_project_id condition to merge_request_metrics
|
||||
items.join_metrics(project_id)
|
||||
else
|
||||
items.join_metrics
|
||||
end
|
||||
|
||||
scope.merge(mr_metrics_scope)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
|||
|
|
@ -425,6 +425,15 @@ module IssuablesHelper
|
|||
}
|
||||
end
|
||||
|
||||
def sidebar_status_data(issuable_sidebar, project)
|
||||
{
|
||||
iid: issuable_sidebar[:iid],
|
||||
issuable_type: issuable_sidebar[:type],
|
||||
full_path: project.full_path,
|
||||
can_edit: issuable_sidebar.dig(:current_user, :can_edit).to_s
|
||||
}
|
||||
end
|
||||
|
||||
def parent
|
||||
@project || @group
|
||||
end
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ module SearchHelper
|
|||
|
||||
if @scope == scope
|
||||
li_class = 'active'
|
||||
count = @search_results.formatted_count(scope)
|
||||
count = @timeout ? 0 : @search_results.formatted_count(scope)
|
||||
else
|
||||
badge_class = 'js-search-count hidden'
|
||||
badge_data = { url: search_count_path(search_params) }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ module SortingHelper
|
|||
sort_value_recently_updated => sort_title_recently_updated,
|
||||
sort_value_popularity => sort_title_popularity,
|
||||
sort_value_priority => sort_title_priority,
|
||||
sort_value_merged_date => sort_title_merged_date,
|
||||
sort_value_merged_recently => sort_title_merged_recently,
|
||||
sort_value_merged_earlier => sort_title_merged_earlier,
|
||||
sort_value_upvotes => sort_title_upvotes,
|
||||
sort_value_contacted_date => sort_title_contacted_date,
|
||||
sort_value_relative_position => sort_title_relative_position,
|
||||
|
|
@ -178,6 +181,7 @@ module SortingHelper
|
|||
sort_value_oldest_updated => sort_value_recently_updated,
|
||||
sort_value_milestone_later => sort_value_milestone,
|
||||
sort_value_due_date_later => sort_value_due_date,
|
||||
sort_value_merged_recently => sort_value_merged_date,
|
||||
sort_value_least_popular => sort_value_popularity
|
||||
}
|
||||
end
|
||||
|
|
@ -190,6 +194,8 @@ module SortingHelper
|
|||
sort_value_milestone => sort_value_milestone_later,
|
||||
sort_value_due_date => sort_value_due_date_later,
|
||||
sort_value_due_date_soon => sort_value_due_date_later,
|
||||
sort_value_merged_date => sort_value_merged_recently,
|
||||
sort_value_merged_earlier => sort_value_merged_recently,
|
||||
sort_value_popularity => sort_value_least_popular,
|
||||
sort_value_most_popular => sort_value_least_popular
|
||||
}.merge(issuable_sort_option_overrides)
|
||||
|
|
@ -210,7 +216,7 @@ module SortingHelper
|
|||
|
||||
def sort_direction_icon(sort_value)
|
||||
case sort_value
|
||||
when sort_value_milestone, sort_value_due_date, /_asc\z/
|
||||
when sort_value_milestone, sort_value_due_date, sort_value_merged_date, /_asc\z/
|
||||
'sort-lowest'
|
||||
else
|
||||
'sort-highest'
|
||||
|
|
|
|||
|
|
@ -26,6 +26,18 @@ module SortingTitlesValuesHelper
|
|||
s_('SortOptions|Label priority')
|
||||
end
|
||||
|
||||
def sort_title_merged_date
|
||||
s_('SortOptions|Merged date')
|
||||
end
|
||||
|
||||
def sort_title_merged_recently
|
||||
s_('SortOptions|Merged recently')
|
||||
end
|
||||
|
||||
def sort_title_merged_earlier
|
||||
s_('SortOptions|Merged earlier')
|
||||
end
|
||||
|
||||
def sort_title_largest_group
|
||||
s_('SortOptions|Largest group')
|
||||
end
|
||||
|
|
@ -175,6 +187,18 @@ module SortingTitlesValuesHelper
|
|||
'label_priority'
|
||||
end
|
||||
|
||||
def sort_value_merged_date
|
||||
'merged_at'
|
||||
end
|
||||
|
||||
def sort_value_merged_recently
|
||||
'merged_at_desc'
|
||||
end
|
||||
|
||||
def sort_value_merged_earlier
|
||||
'merged_at_asc'
|
||||
end
|
||||
|
||||
def sort_value_largest_group
|
||||
'storage_size_desc'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -426,8 +426,15 @@ class Issue < ApplicationRecord
|
|||
end
|
||||
|
||||
def check_for_spam?
|
||||
publicly_visible? &&
|
||||
(title_changed? || description_changed? || confidential_changed?)
|
||||
# content created via support bots is always checked for spam, EVEN if
|
||||
# the issue is not publicly visible and/or confidential
|
||||
return true if author.support_bot? && spammable_attribute_changed?
|
||||
|
||||
# Only check for spam on issues which are publicly visible (and thus indexed in search engines)
|
||||
return false unless publicly_visible?
|
||||
|
||||
# Only check for spam if certain attributes have changed
|
||||
spammable_attribute_changed?
|
||||
end
|
||||
|
||||
def as_json(options = {})
|
||||
|
|
@ -515,6 +522,14 @@ class Issue < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def spammable_attribute_changed?
|
||||
title_changed? ||
|
||||
description_changed? ||
|
||||
# NOTE: We need to check them for spam when issues are made non-confidential, because spam
|
||||
# may have been added while they were confidential and thus not being checked for spam.
|
||||
confidential_changed?(from: true, to: false)
|
||||
end
|
||||
|
||||
# Ensure that the metrics association is safely created and respecting the unique constraint on issue_id
|
||||
override :ensure_metrics
|
||||
def ensure_metrics
|
||||
|
|
|
|||
|
|
@ -300,6 +300,11 @@ class MergeRequest < ApplicationRecord
|
|||
|
||||
query = joins(:metrics)
|
||||
|
||||
if !target_project_id && self.where_values_hash["target_project_id"]
|
||||
target_project_id = self.where_values_hash["target_project_id"]
|
||||
query = query.unscope(where: :target_project_id)
|
||||
end
|
||||
|
||||
project_condition = if target_project_id
|
||||
MergeRequest::Metrics.arel_table[:target_project_id].eq(target_project_id)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
- search_bar_classes = 'search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4'
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
= render partial: 'search/results_status', locals: { search_service: @search_service } unless @search_objects.to_a.empty?
|
||||
|
||||
- if @search_objects.to_a.empty?
|
||||
.gl-md-display-flex
|
||||
- if %w(issues merge_requests).include?(@scope)
|
||||
#js-search-sidebar{ class: search_bar_classes }
|
||||
.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
|
||||
.results.gl-md-display-flex.gl-mt-3
|
||||
- if %w(issues merge_requests).include?(@scope)
|
||||
#js-search-sidebar{ class: search_bar_classes }
|
||||
.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
|
||||
- if @timeout
|
||||
= render partial: "search/results/timeout"
|
||||
- elsif @search_objects.to_a.empty?
|
||||
= render partial: "search/results/empty"
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
- else
|
||||
= render partial: 'search/results_status', locals: { search_service: @search_service }
|
||||
= render_if_exists 'shared/promotions/promote_advanced_search'
|
||||
|
||||
.results.gl-md-display-flex.gl-mt-3
|
||||
- if %w(issues merge_requests).include?(@scope)
|
||||
#js-search-sidebar{ class: search_bar_classes }
|
||||
.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
|
||||
- else
|
||||
- if @scope == 'commits'
|
||||
%ul.content-list.commit-list
|
||||
= render partial: "search/results/commit", collection: @search_objects
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
.gl-display-flex.gl-flex-direction-column.gl-align-items-center
|
||||
%div
|
||||
.svg-content.svg-150
|
||||
= image_tag 'illustrations/search-timeout-md.svg'
|
||||
%div
|
||||
%h4.gl-text-center.gl-font-weight-bold= _('Your search timed out')
|
||||
%p.gl-text-center= _('To resolve this, try to:')
|
||||
%ul
|
||||
%li= html_escape(_('Refine your search criteria (select a %{strong_open}group%{strong_close} and %{strong_open}project%{strong_close} when possible)')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
|
||||
%li= html_escape(_('Use double quotes for multiple keywords, such as %{code_open}"your search"%{code_close}')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
#js-severity
|
||||
|
||||
- if issuable_sidebar.dig(:features_available, :health_status)
|
||||
.js-sidebar-status-entry-point
|
||||
.js-sidebar-status-entry-point{ data: sidebar_status_data(issuable_sidebar, @project) }
|
||||
|
||||
- if issuable_sidebar.has_key?(:confidential)
|
||||
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
- sort_value = @sort
|
||||
- sort_title = issuable_sort_option_title(sort_value)
|
||||
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
|
||||
- viewing_merge_requests = controller.controller_name == 'merge_requests'
|
||||
|
||||
.dropdown.inline.gl-ml-3.issue-sort-dropdown
|
||||
.btn-group{ role: 'group' }
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
= sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
|
||||
= sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
|
||||
= sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
|
||||
= sortable_item(sort_title_merged_date, page_filter_path(sort: sort_value_merged_date), sort_title) if viewing_merge_requests
|
||||
= sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues
|
||||
= render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
|
||||
= issuable_sort_direction_button(sort_value)
|
||||
|
|
|
|||
|
|
@ -49,15 +49,11 @@ module ContainerExpirationPolicies
|
|||
end
|
||||
|
||||
def remaining_work_count
|
||||
total_count = cleanup_scheduled_count + cleanup_unfinished_count
|
||||
count = cleanup_scheduled_count
|
||||
|
||||
log_info(
|
||||
cleanup_scheduled_count: cleanup_scheduled_count,
|
||||
cleanup_unfinished_count: cleanup_unfinished_count,
|
||||
cleanup_total_count: total_count
|
||||
)
|
||||
return count if count > max_running_jobs
|
||||
|
||||
total_count
|
||||
count + cleanup_unfinished_count
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
|
|||
process_stale_ongoing_cleanups
|
||||
disable_policies_without_container_repositories
|
||||
throttling_enabled? ? perform_throttled : perform_unthrottled
|
||||
log_counts
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -28,6 +29,26 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
|
|||
end
|
||||
end
|
||||
|
||||
def log_counts
|
||||
use_replica_if_available do
|
||||
required_count = ContainerRepository.requiring_cleanup.count
|
||||
unfinished_count = ContainerRepository.with_unfinished_cleanup.count
|
||||
|
||||
log_extra_metadata_on_done(:cleanup_required_count, required_count)
|
||||
log_extra_metadata_on_done(:cleanup_unfinished_count, unfinished_count)
|
||||
log_extra_metadata_on_done(:cleanup_total_count, required_count + unfinished_count)
|
||||
end
|
||||
end
|
||||
|
||||
# data_consistency :delayed not used as this is a cron job and those jobs are
|
||||
# not perfomed with a delay
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63635#note_603771207
|
||||
def use_replica_if_available(&blk)
|
||||
return yield unless ::Gitlab::Database::LoadBalancing.enable?
|
||||
|
||||
::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&blk)
|
||||
end
|
||||
|
||||
def process_stale_ongoing_cleanups
|
||||
threshold = delete_tags_service_timeout.seconds + 30.minutes
|
||||
ContainerRepository.with_stale_ongoing_cleanup(threshold.ago)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class CreateContainerReposOnExpCleanupStatusProjectIdStartDateIndex < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
OLD_INDEX_NAME = 'idx_container_repositories_on_exp_cleanup_status_and_start_date'
|
||||
NEW_INDEX_NAME = 'idx_container_repos_on_exp_cleanup_status_project_id_start_date'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(:container_repositories, [:expiration_policy_cleanup_status, :project_id, :expiration_policy_started_at], name: NEW_INDEX_NAME)
|
||||
remove_concurrent_index(:container_repositories, [:expiration_policy_cleanup_status, :expiration_policy_started_at], name: OLD_INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index(:container_repositories, [:expiration_policy_cleanup_status, :expiration_policy_started_at], name: OLD_INDEX_NAME)
|
||||
remove_concurrent_index(:container_repositories, [:expiration_policy_cleanup_status, :project_id, :expiration_policy_started_at], name: NEW_INDEX_NAME)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
c33dd2c63d5a8c6e3c2f49e640b1780734b4bfca88378fac67ea5f5bd24fb2b4
|
||||
|
|
@ -22536,7 +22536,7 @@ CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at ON container_e
|
|||
|
||||
CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at_enabled ON container_expiration_policies USING btree (project_id, next_run_at, enabled);
|
||||
|
||||
CREATE INDEX idx_container_repositories_on_exp_cleanup_status_and_start_date ON container_repositories USING btree (expiration_policy_cleanup_status, expiration_policy_started_at);
|
||||
CREATE INDEX idx_container_repos_on_exp_cleanup_status_project_id_start_date ON container_repositories USING btree (expiration_policy_cleanup_status, project_id, expiration_policy_started_at);
|
||||
|
||||
CREATE INDEX idx_deployment_clusters_on_cluster_id_and_kubernetes_namespace ON deployment_clusters USING btree (cluster_id, kubernetes_namespace);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ page, with these behaviors:
|
|||
1. It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
|
||||
- contains the string 'OOO', 'PTO', 'Parental Leave', or 'Friends and Family'
|
||||
- emoji is `:palm_tree:`, `:beach:`, `:beach_umbrella:`, `:beach_with_umbrella:`, `:ferris_wheel:`, `:thermometer:`, `:face_with_thermometer:`, `:red_circle:`, `:bulb:`, `:sun_with_face:`.
|
||||
- GitLab user busy indicator is set to true
|
||||
1. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
|
||||
are three times as likely to be picked as other reviewers.
|
||||
1. Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ graph RL;
|
|||
click 1-5 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914312&udv=0"
|
||||
1-6["setup-test-env (4 minutes)"];
|
||||
click 1-6 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914315&udv=0"
|
||||
1-7["review-stop-failed-deployment"];
|
||||
1-7["review-delete-deployment"];
|
||||
1-8["dependency_scanning"];
|
||||
1-9["qa:internal, qa:internal-as-if-foss"];
|
||||
1-11["qa:selectors, qa:selectors-as-if-foss"];
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ your integration.
|
|||
- *For GitLab versions 13.0 and later* **and** *Jira versions 8.14 and later,* use the
|
||||
generated `Redirect URL` from
|
||||
[Linking GitLab accounts with Jira](https://confluence.atlassian.com/adminjiraserver/linking-gitlab-accounts-1027142272.html).
|
||||
- *For GitLab versions 13.0 and later* **and** *Jira Cloud,* use `https://<gitlab.example.com>/login/oauth/callback`.
|
||||
- *For GitLab versions 11.3 and later,* use `https://<gitlab.example.com>/login/oauth/callback`.
|
||||
If you use GitLab.com, the URL is `https://gitlab.com/login/oauth/callback`.
|
||||
- *For GitLab versions 11.2 and earlier,* use
|
||||
|
|
@ -89,7 +90,7 @@ your integration.
|
|||
|
||||
## Configure Jira for DVCS
|
||||
|
||||
If you use Jira Cloud and GitLab.com, use the [GitLab for Jira app](connect-app.md)
|
||||
If you use Jira Cloud, use the [GitLab for Jira app](connect-app.md)
|
||||
unless you specifically need the DVCS Connector.
|
||||
|
||||
Configure this connection when you want to import all GitLab commits and branches,
|
||||
|
|
|
|||
|
|
@ -9,36 +9,33 @@ type: reference
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2.
|
||||
|
||||
GitLab administrators can configure the group where all the custom project
|
||||
templates are sourced.
|
||||
GitLab administrators can set a group to be the source of project templates that are
|
||||
selectable when a new project is created on the instance. These templates can be selected
|
||||
when you go to **New project > Create from template** and select the **Instance** tab.
|
||||
|
||||
Every project directly under the group namespace will be
|
||||
available to the user if they have access to them. For example:
|
||||
Every project in the group, but not its subgroups, can be selected when a new project
|
||||
is created, based on the user's access permissions:
|
||||
|
||||
- Public projects, in the group will be available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions)
|
||||
- Public projects can be selected by any signed-in user as a template for a new project,
|
||||
if all enabled [project features](../project/settings/index.md#sharing-and-permissions)
|
||||
except for GitLab Pages are set to **Everyone With Access**.
|
||||
- Private projects will be available only if the user is a member of the project.
|
||||
- Private projects can be selected only by users who are members of the projects.
|
||||
|
||||
Repository and database information that are copied over to each new project are
|
||||
identical to the data exported with the
|
||||
[GitLab Project Import/Export](../project/settings/import_export.md).
|
||||
identical to the data exported with the [GitLab Project Import/Export](../project/settings/import_export.md).
|
||||
|
||||
NOTE:
|
||||
To set project templates at a group level,
|
||||
see [Custom group-level project templates](../group/custom_project_templates.md).
|
||||
To set project templates at the group level, see [Custom group-level project templates](../group/custom_project_templates.md).
|
||||
|
||||
## Configuring
|
||||
## Select instance-level project template group
|
||||
|
||||
GitLab administrators can configure a GitLab group that serves as template
|
||||
source for an entire GitLab instance:
|
||||
To select the group to use as the source for the project templates:
|
||||
|
||||
1. On the top bar, navigate to **Menu > Admin > Settings > Templates**.
|
||||
1. Expand **Custom project templates**.
|
||||
1. Select a group to use.
|
||||
1. Select **Save changes**.
|
||||
|
||||
NOTE:
|
||||
Projects below subgroups of the template group are **not** supported.
|
||||
Projects in subgroups of the template group are **not** included in the template list.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ and to-do items are assigned to you:
|
|||
|
||||

|
||||
|
||||
- **(issues)** **Issues**: The open issues assigned to you.
|
||||
- **(merge-request-open)** **Merge requests**: The [merge requests](../project/merge_requests/index.md) assigned to you.
|
||||
- **(todo-done)** **To-do items**: The [to-do items](../todos.md) assigned to you.
|
||||
- **{issues}** **Issues**: The open issues assigned to you.
|
||||
- **{merge-request-open}** **Merge requests**: The [merge requests](../project/merge_requests/index.md) assigned to you.
|
||||
- **{todo-done}** **To-do items**: The [to-do items](../todos.md) assigned to you.
|
||||
|
||||
When you click **Issues**, GitLab shows the opened issues assigned to you:
|
||||
|
||||
|
|
|
|||
|
|
@ -5,27 +5,29 @@ module Gitlab
|
|||
module LoadBalancing
|
||||
class SidekiqClientMiddleware
|
||||
def call(worker_class, job, _queue, _redis_pool)
|
||||
# Mailers can't be constantized
|
||||
worker_class = worker_class.to_s.safe_constantize
|
||||
|
||||
mark_data_consistency_location(worker_class, job)
|
||||
if load_balancing_enabled?(worker_class)
|
||||
job['worker_data_consistency'] = worker_class.get_data_consistency
|
||||
set_data_consistency_location!(job) unless location_already_provided?(job)
|
||||
else
|
||||
job['worker_data_consistency'] = ::WorkerAttributes::DEFAULT_DATA_CONSISTENCY
|
||||
end
|
||||
|
||||
yield
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_data_consistency_location(worker_class, job)
|
||||
# Mailers can't be constantized
|
||||
return unless worker_class
|
||||
return unless worker_class.include?(::ApplicationWorker)
|
||||
return unless worker_class.get_data_consistency_feature_flag_enabled?
|
||||
|
||||
return if location_already_provided?(job)
|
||||
|
||||
job['worker_data_consistency'] = worker_class.get_data_consistency
|
||||
|
||||
return unless worker_class.utilizes_load_balancing_capabilities?
|
||||
def load_balancing_enabled?(worker_class)
|
||||
worker_class &&
|
||||
worker_class.include?(::ApplicationWorker) &&
|
||||
worker_class.utilizes_load_balancing_capabilities? &&
|
||||
worker_class.get_data_consistency_feature_flag_enabled?
|
||||
end
|
||||
|
||||
def set_data_consistency_location!(job)
|
||||
if Session.current.use_primary?
|
||||
job['database_write_location'] = load_balancer.primary_write_location
|
||||
else
|
||||
|
|
|
|||
|
|
@ -10,17 +10,14 @@ module Gitlab
|
|||
worker_class = worker.class
|
||||
strategy = select_load_balancing_strategy(worker_class, job)
|
||||
|
||||
# This is consumed by ServerMetrics and StructuredLogger to emit metrics so we only
|
||||
# make this available when load-balancing is actually utilized.
|
||||
job['load_balancing_strategy'] = strategy.to_s if load_balancing_available?(worker_class)
|
||||
job['load_balancing_strategy'] = strategy.to_s
|
||||
|
||||
case strategy
|
||||
when :primary, :retry_primary
|
||||
if use_primary?(strategy)
|
||||
Session.current.use_primary!
|
||||
when :retry_replica
|
||||
elsif strategy == :retry
|
||||
raise JobReplicaNotUpToDate, "Sidekiq job #{worker_class} JID-#{job['jid']} couldn't use the replica."\
|
||||
" Replica was not up to date."
|
||||
when :replica
|
||||
" Replica was not up to date."
|
||||
else
|
||||
# this means we selected an up-to-date replica, but there is nothing to do in this case.
|
||||
end
|
||||
|
||||
|
|
@ -36,17 +33,24 @@ module Gitlab
|
|||
Session.clear_session
|
||||
end
|
||||
|
||||
def use_primary?(strategy)
|
||||
strategy.start_with?('primary')
|
||||
end
|
||||
|
||||
def select_load_balancing_strategy(worker_class, job)
|
||||
return :primary unless load_balancing_available?(worker_class)
|
||||
|
||||
location = job['database_write_location'] || job['database_replica_location']
|
||||
return :primary unless location
|
||||
return :primary_no_wal unless location
|
||||
|
||||
if replica_caught_up?(location)
|
||||
:replica
|
||||
elsif worker_class.get_data_consistency == :delayed
|
||||
not_yet_retried?(job) ? :retry_replica : :retry_primary
|
||||
# Happy case: we can read from a replica.
|
||||
retried_before?(worker_class, job) ? :replica_retried : :replica
|
||||
elsif can_retry?(worker_class, job)
|
||||
# Optimistic case: The worker allows retries and we have retries left.
|
||||
:retry
|
||||
else
|
||||
# Sad case: we need to fall back to the primary.
|
||||
:primary
|
||||
end
|
||||
end
|
||||
|
|
@ -57,6 +61,14 @@ module Gitlab
|
|||
worker_class.get_data_consistency_feature_flag_enabled?
|
||||
end
|
||||
|
||||
def can_retry?(worker_class, job)
|
||||
worker_class.get_data_consistency == :delayed && not_yet_retried?(job)
|
||||
end
|
||||
|
||||
def retried_before?(worker_class, job)
|
||||
worker_class.get_data_consistency == :delayed && !not_yet_retried?(job)
|
||||
end
|
||||
|
||||
def not_yet_retried?(job)
|
||||
# if `retry_count` is `nil` it indicates that this job was never retried
|
||||
# the `0` indicates that this is a first retry
|
||||
|
|
|
|||
|
|
@ -2409,6 +2409,9 @@ msgstr ""
|
|||
msgid "AdminSettings|Select a CI/CD template"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Select a group to use as the source for instance-level project templates."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Service template allows you to set default values for integrations"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2430,6 +2433,9 @@ msgstr ""
|
|||
msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|The projects in this group can be selected as templates for new projects created on the instance. %{link_start}Learn more.%{link_end} "
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|The template for the required pipeline configuration can be one of the GitLab-provided templates, or a custom template added to an instance template repository. %{link_start}How do I create an instance template repository?%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10218,6 +10224,12 @@ msgstr ""
|
|||
msgid "DastProfiles|Website"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|You can either choose a passive scan or validate the target site in your chosen site profile. %{docsLinkStart}Learn more about site validation.%{docsLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|You cannot run an active scan against an unvalidated site."
|
||||
msgstr ""
|
||||
|
||||
msgid "DastSiteValidation|Copy HTTP header to clipboard"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11838,9 +11850,6 @@ msgstr ""
|
|||
msgid "Edit issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit public deploy key"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12855,6 +12864,9 @@ msgstr ""
|
|||
msgid "Error occurred when saving reviewers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error occurred while updating the %{issuableType} status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error occurred while updating the issue status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15900,9 +15912,6 @@ msgstr ""
|
|||
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupTemplate|The selected group's repositories and databases are copied into the projects created in this group. %{link_start}What should I add to my template group?%{link_end} "
|
||||
msgstr ""
|
||||
|
||||
msgid "Groups"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18318,9 +18327,6 @@ msgstr ""
|
|||
msgid "Iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration cadences"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration changed to"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18363,6 +18369,9 @@ msgstr ""
|
|||
msgid "Iterations|Edit cadence"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Edit iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Edit iteration cadence"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18372,12 +18381,18 @@ msgstr ""
|
|||
msgid "Iterations|Future iterations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Iteration cadences"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Iteration scheduling will be handled automatically"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Move incomplete issues to the next iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|New iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|New iteration cadence"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21948,9 +21963,6 @@ msgstr ""
|
|||
msgid "New issue title"
|
||||
msgstr ""
|
||||
|
||||
msgid "New iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "New iteration created"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26953,6 +26965,9 @@ msgstr ""
|
|||
msgid "References"
|
||||
msgstr ""
|
||||
|
||||
msgid "Refine your search criteria (select a %{strong_open}group%{strong_close} and %{strong_open}project%{strong_close} when possible)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29341,9 +29356,6 @@ msgstr ""
|
|||
msgid "Select a shared template repository for all projects on this instance."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a subgroup to use as a template when creating new projects in the group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a template repository"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29380,6 +29392,9 @@ msgstr ""
|
|||
msgid "Select file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select group or project"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30533,6 +30548,9 @@ msgstr ""
|
|||
msgid "Something went wrong while setting %{issuableType} confidentiality."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while setting %{issuableType} health status."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while setting %{issuableType} notifications."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30653,6 +30671,15 @@ msgstr ""
|
|||
msgid "SortOptions|Manual"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Merged date"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Merged earlier"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Merged recently"
|
||||
msgstr ""
|
||||
|
||||
msgid "SortOptions|Milestone due date"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34218,6 +34245,9 @@ msgstr ""
|
|||
msgid "To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "To resolve this, try to:"
|
||||
msgstr ""
|
||||
|
||||
msgid "To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35474,6 +35504,9 @@ msgstr ""
|
|||
msgid "Use custom color #FF0000"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use double quotes for multiple keywords, such as %{code_open}\"your search\"%{code_close}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use hashed storage"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38048,6 +38081,9 @@ msgstr ""
|
|||
msgid "Your search didn't match any commits. Try a different query."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your search timed out"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your sign-in page is %{url}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ function delete_release() {
|
|||
return
|
||||
fi
|
||||
|
||||
delete_k8s_release_namespace
|
||||
if deploy_exists "${namespace}" "${release}"; then
|
||||
helm uninstall --namespace="${namespace}" "${release}"
|
||||
fi
|
||||
}
|
||||
|
||||
function delete_failed_release() {
|
||||
|
|
@ -66,7 +68,7 @@ function delete_failed_release() {
|
|||
# Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
|
||||
if previous_deploy_failed "${namespace}" "${release}" ; then
|
||||
echoinfo "Review App deployment in bad state, cleaning up namespace ${release}"
|
||||
delete_release
|
||||
delete_k8s_release_namespace
|
||||
else
|
||||
echoinfo "Review App deployment in good state"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ RSpec.describe 'Merge requests > User lists merge requests' do
|
|||
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
|
||||
created_at: 1.minute.ago,
|
||||
updated_at: 1.minute.ago)
|
||||
create(:merge_request,
|
||||
@fix.metrics.update_column(:merged_at, 10.seconds.ago)
|
||||
|
||||
@markdown = create(:merge_request,
|
||||
title: 'markdown',
|
||||
source_project: project,
|
||||
source_branch: 'markdown',
|
||||
|
|
@ -32,12 +34,15 @@ RSpec.describe 'Merge requests > User lists merge requests' do
|
|||
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
|
||||
created_at: 2.minutes.ago,
|
||||
updated_at: 2.minutes.ago)
|
||||
create(:merge_request,
|
||||
@markdown.metrics.update_column(:merged_at, 50.seconds.ago)
|
||||
|
||||
@merge_test = create(:merge_request,
|
||||
title: 'merge-test',
|
||||
source_project: project,
|
||||
source_branch: 'merge-test',
|
||||
created_at: 3.minutes.ago,
|
||||
updated_at: 10.seconds.ago)
|
||||
@merge_test.metrics.update_column(:merged_at, 10.seconds.ago)
|
||||
end
|
||||
|
||||
context 'merge request reviewers' do
|
||||
|
|
@ -102,6 +107,13 @@ RSpec.describe 'Merge requests > User lists merge requests' do
|
|||
expect(count_merge_requests).to eq(3)
|
||||
end
|
||||
|
||||
it 'sorts by merged at' do
|
||||
visit_merge_requests(project, sort: sort_value_merged_date)
|
||||
|
||||
expect(first_merge_request).to include('markdown')
|
||||
expect(count_merge_requests).to eq(3)
|
||||
end
|
||||
|
||||
it 'filters on one label and sorts by due date' do
|
||||
label = create(:label, project: project)
|
||||
create(:label_link, label: label, target: @fix)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ RSpec.describe 'User searches for code' do
|
|||
end
|
||||
|
||||
include_examples 'top right search form'
|
||||
include_examples 'search timeouts', 'blobs'
|
||||
|
||||
it 'finds code' do
|
||||
fill_in('dashboard_search', with: 'rspec')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ RSpec.describe 'User searches for comments' do
|
|||
visit(project_path(project))
|
||||
end
|
||||
|
||||
include_examples 'search timeouts', 'notes'
|
||||
|
||||
context 'when a comment is in commits' do
|
||||
context 'when comment belongs to an invalid commit' do
|
||||
let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ RSpec.describe 'User searches for commits', :js do
|
|||
visit(search_path(project_id: project.id))
|
||||
end
|
||||
|
||||
include_examples 'search timeouts', 'commits'
|
||||
|
||||
context 'when searching by SHA' do
|
||||
it 'finds a commit and redirects to its page' do
|
||||
submit_search(sha)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ RSpec.describe 'User searches for issues', :js do
|
|||
end
|
||||
|
||||
include_examples 'top right search form'
|
||||
include_examples 'search timeouts', 'issues'
|
||||
|
||||
it 'finds an issue' do
|
||||
search_for_issue(issue1.title)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ RSpec.describe 'User searches for merge requests', :js do
|
|||
end
|
||||
|
||||
include_examples 'top right search form'
|
||||
include_examples 'search timeouts', 'merge_requests'
|
||||
|
||||
it 'finds a merge request' do
|
||||
search_for_mr(merge_request1.title)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RSpec.describe 'User searches for milestones', :js do
|
|||
end
|
||||
|
||||
include_examples 'top right search form'
|
||||
include_examples 'search timeouts', 'milestones'
|
||||
|
||||
it 'finds a milestone' do
|
||||
fill_in('dashboard_search', with: milestone1.title)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ RSpec.describe 'User searches for projects', :js do
|
|||
end
|
||||
|
||||
include_examples 'top right search form'
|
||||
include_examples 'search timeouts', 'projects'
|
||||
|
||||
it 'finds a project' do
|
||||
visit(search_path)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ RSpec.describe 'User searches for users' do
|
|||
sign_in(user1)
|
||||
end
|
||||
|
||||
include_examples 'search timeouts', 'users'
|
||||
|
||||
context 'when on the dashboard' do
|
||||
it 'finds the user', :js do
|
||||
visit dashboard_projects_path
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RSpec.describe 'User searches for wiki pages', :js do
|
|||
end
|
||||
|
||||
include_examples 'top right search form'
|
||||
include_examples 'search timeouts', 'wiki_blobs'
|
||||
|
||||
shared_examples 'search wiki blobs' do
|
||||
it 'finds a page' do
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
|
|||
cssclasses="mr-2"
|
||||
filemode=""
|
||||
filename="foo/bar/dummy.md"
|
||||
size="18"
|
||||
size="16"
|
||||
/>
|
||||
|
||||
<strong
|
||||
|
|
|
|||
|
|
@ -133,14 +133,6 @@ describe('IssuableShowRoot', () => {
|
|||
expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('component emits `sidebar-toggle` event bubbled via issuable-sidebar', () => {
|
||||
const issuableSidebar = wrapper.find(IssuableSidebar);
|
||||
|
||||
issuableSidebar.vm.$emit('sidebar-toggle', true);
|
||||
|
||||
expect(wrapper.emitted('sidebar-toggle')).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each(['keydown-title', 'keydown-description'])(
|
||||
'component emits `%s` event with event object and issuableMeta params via issuable-body',
|
||||
(eventName) => {
|
||||
|
|
|
|||
|
|
@ -1,88 +1,80 @@
|
|||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Cookies from 'js-cookie';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import IssuableSidebarRoot from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
|
||||
import { USER_COLLAPSED_GUTTER_COOKIE } from '~/issuable_sidebar/constants';
|
||||
|
||||
const createComponent = (expanded = true) =>
|
||||
shallowMount(IssuableSidebarRoot, {
|
||||
propsData: {
|
||||
expanded,
|
||||
},
|
||||
const MOCK_LAYOUT_PAGE_CLASS = 'layout-page';
|
||||
|
||||
const createComponent = () => {
|
||||
setFixtures(`<div class="${MOCK_LAYOUT_PAGE_CLASS}"></div>`);
|
||||
|
||||
return shallowMountExtended(IssuableSidebarRoot, {
|
||||
slots: {
|
||||
'right-sidebar-items': `
|
||||
<button class="js-todo">Todo</button>
|
||||
`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('IssuableSidebarRoot', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
const findToggleSidebarButton = () => wrapper.findByTestId('toggle-right-sidebar-button');
|
||||
|
||||
const assertPageLayoutClasses = ({ isExpanded }) => {
|
||||
const { classList } = document.querySelector(`.${MOCK_LAYOUT_PAGE_CLASS}`);
|
||||
if (isExpanded) {
|
||||
expect(classList).toContain('right-sidebar-expanded');
|
||||
expect(classList).not.toContain('right-sidebar-collapsed');
|
||||
} else {
|
||||
expect(classList).toContain('right-sidebar-collapsed');
|
||||
expect(classList).not.toContain('right-sidebar-expanded');
|
||||
}
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
describe('isExpanded', () => {
|
||||
it('emits `sidebar-toggle` event on component', async () => {
|
||||
wrapper.setData({
|
||||
isExpanded: false,
|
||||
});
|
||||
describe('when sidebar is expanded', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Cookies, 'set').mockImplementation(jest.fn());
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue(false);
|
||||
jest.spyOn(bp, 'isDesktop').mockReturnValue(true);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('sidebar-toggle')).toBeTruthy();
|
||||
expect(wrapper.emitted('sidebar-toggle')[0]).toEqual([
|
||||
{
|
||||
expanded: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('updatePageContainerClass', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures('<div class="layout-page"></div>');
|
||||
});
|
||||
|
||||
it.each`
|
||||
isExpanded | layoutPageClass
|
||||
${true} | ${'right-sidebar-expanded'}
|
||||
${false} | ${'right-sidebar-collapsed'}
|
||||
`(
|
||||
'set class $layoutPageClass to container element when `isExpanded` prop is $isExpanded',
|
||||
async ({ isExpanded, layoutPageClass }) => {
|
||||
wrapper.setData({
|
||||
isExpanded,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.updatePageContainerClass();
|
||||
|
||||
expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe(
|
||||
true,
|
||||
);
|
||||
},
|
||||
);
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
describe('handleWindowResize', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper.setData({
|
||||
userExpanded: true,
|
||||
});
|
||||
it('renders component container element with class `right-sidebar-expanded`', () => {
|
||||
expect(wrapper.classes()).toContain('right-sidebar-expanded');
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
it('sets layout class to reflect expanded state', () => {
|
||||
assertPageLayoutClasses({ isExpanded: true });
|
||||
});
|
||||
|
||||
it('renders sidebar toggle button with text and icon', () => {
|
||||
const buttonEl = findToggleSidebarButton();
|
||||
|
||||
expect(buttonEl.exists()).toBe(true);
|
||||
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
|
||||
expect(buttonEl.find('span').text()).toBe('Collapse sidebar');
|
||||
expect(wrapper.findByTestId('icon-collapse').isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when collapsing the sidebar', () => {
|
||||
it('updates "collapsed_gutter" cookie value and layout classes', async () => {
|
||||
await findToggleSidebarButton().trigger('click');
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith(USER_COLLAPSED_GUTTER_COOKIE, true);
|
||||
assertPageLayoutClasses({ isExpanded: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window `resize` event is triggered', () => {
|
||||
it.each`
|
||||
breakpoint | isExpandedValue
|
||||
${'xs'} | ${false}
|
||||
|
|
@ -91,109 +83,49 @@ describe('IssuableSidebarRoot', () => {
|
|||
${'lg'} | ${true}
|
||||
${'xl'} | ${true}
|
||||
`(
|
||||
'sets `isExpanded` prop to $isExpandedValue only when current screen size is `lg` or `xl`',
|
||||
'sets page layout classes correctly when current screen size is `$breakpoint`',
|
||||
async ({ breakpoint, isExpandedValue }) => {
|
||||
jest.spyOn(bp, 'isDesktop').mockReturnValue(breakpoint === 'lg' || breakpoint === 'xl');
|
||||
|
||||
wrapper.vm.handleWindowResize();
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.isExpanded).toBe(isExpandedValue);
|
||||
assertPageLayoutClasses({ isExpanded: isExpandedValue });
|
||||
},
|
||||
);
|
||||
|
||||
it('calls `updatePageContainerClass` method', () => {
|
||||
jest.spyOn(wrapper.vm, 'updatePageContainerClass');
|
||||
|
||||
wrapper.vm.handleWindowResize();
|
||||
|
||||
expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleToggleSidebarClick', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(Cookies, 'set').mockImplementation(jest.fn());
|
||||
wrapper.setData({
|
||||
isExpanded: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('flips value of `isExpanded`', () => {
|
||||
wrapper.vm.handleToggleSidebarClick();
|
||||
|
||||
expect(wrapper.vm.isExpanded).toBe(false);
|
||||
expect(wrapper.vm.userExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it('updates "collapsed_gutter" cookie value', () => {
|
||||
wrapper.vm.handleToggleSidebarClick();
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith('collapsed_gutter', true);
|
||||
});
|
||||
|
||||
it('calls `updatePageContainerClass` method', () => {
|
||||
jest.spyOn(wrapper.vm, 'updatePageContainerClass');
|
||||
|
||||
wrapper.vm.handleWindowResize();
|
||||
|
||||
expect(wrapper.vm.updatePageContainerClass).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
describe('sidebar expanded', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper.setData({
|
||||
isExpanded: true,
|
||||
});
|
||||
describe('when sidebar is collapsed', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Cookies, 'get').mockReturnValue(true);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders component container element with class `right-sidebar-expanded` when `isExpanded` prop is true', () => {
|
||||
expect(wrapper.classes()).toContain('right-sidebar-expanded');
|
||||
});
|
||||
|
||||
it('renders sidebar toggle button with text and icon', () => {
|
||||
const buttonEl = wrapper.find('button');
|
||||
|
||||
expect(buttonEl.exists()).toBe(true);
|
||||
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
|
||||
expect(buttonEl.find('span').text()).toBe('Collapse sidebar');
|
||||
expect(buttonEl.find('[data-testid="icon-collapse"]').isVisible()).toBe(true);
|
||||
});
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
describe('sidebar collapsed', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper.setData({
|
||||
isExpanded: false,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders component container element with class `right-sidebar-collapsed` when `isExpanded` prop is false', () => {
|
||||
expect(wrapper.classes()).toContain('right-sidebar-collapsed');
|
||||
});
|
||||
|
||||
it('renders sidebar toggle button with text and icon', () => {
|
||||
const buttonEl = wrapper.find('button');
|
||||
|
||||
expect(buttonEl.exists()).toBe(true);
|
||||
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
|
||||
expect(buttonEl.find('[data-testid="icon-expand"]').isVisible()).toBe(true);
|
||||
});
|
||||
it('renders component container element with class `right-sidebar-collapsed`', () => {
|
||||
expect(wrapper.classes()).toContain('right-sidebar-collapsed');
|
||||
});
|
||||
|
||||
it('renders sidebar items', () => {
|
||||
const sidebarItemsEl = wrapper.find('[data-testid="sidebar-items"]');
|
||||
|
||||
expect(sidebarItemsEl.exists()).toBe(true);
|
||||
expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true);
|
||||
it('sets layout class to reflect collapsed state', () => {
|
||||
assertPageLayoutClasses({ isExpanded: false });
|
||||
});
|
||||
|
||||
it('renders sidebar toggle button with text and icon', () => {
|
||||
const buttonEl = findToggleSidebarButton();
|
||||
|
||||
expect(buttonEl.exists()).toBe(true);
|
||||
expect(buttonEl.attributes('title')).toBe('Toggle sidebar');
|
||||
expect(wrapper.findByTestId('icon-expand').isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders slotted sidebar items', () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
const sidebarItemsEl = wrapper.findByTestId('sidebar-items');
|
||||
|
||||
expect(sidebarItemsEl.exists()).toBe(true);
|
||||
expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,12 +5,27 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
||||
let(:middleware) { described_class.new }
|
||||
|
||||
let(:load_balancer) { double.as_null_object }
|
||||
let(:worker_class) { 'TestDataConsistencyWorker' }
|
||||
let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e" } }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
skip_default_enabled_yaml_check
|
||||
allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer).and_return(load_balancer)
|
||||
end
|
||||
|
||||
after do
|
||||
Gitlab::Database::LoadBalancing::Session.clear_session
|
||||
end
|
||||
|
||||
def run_middleware
|
||||
middleware.call(worker_class, job, nil, nil) {}
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
shared_context 'data consistency worker class' do |data_consistency, feature_flag|
|
||||
let(:expected_consistency) { data_consistency }
|
||||
let(:worker_class) do
|
||||
Class.new do
|
||||
def self.name
|
||||
|
|
@ -31,13 +46,23 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'job data consistency' do
|
||||
it "sets job data consistency" do
|
||||
run_middleware
|
||||
|
||||
expect(job['worker_data_consistency']).to eq(expected_consistency)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'does not pass database locations' do
|
||||
it 'does not pass database locations', :aggregate_failures do
|
||||
middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
|
||||
run_middleware
|
||||
|
||||
expect(job['database_replica_location']).to be_nil
|
||||
expect(job['database_write_location']).to be_nil
|
||||
end
|
||||
|
||||
include_examples 'job data consistency'
|
||||
end
|
||||
|
||||
shared_examples_for 'mark data consistency location' do |data_consistency|
|
||||
|
|
@ -45,7 +70,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
|
||||
let(:location) { '0/D525E3A8' }
|
||||
|
||||
context 'when feature flag load_balancing_for_sidekiq is disabled' do
|
||||
context 'when feature flag is disabled' do
|
||||
let(:expected_consistency) { :always }
|
||||
|
||||
before do
|
||||
stub_feature_flags(load_balancing_for_test_data_consistency_worker: false)
|
||||
end
|
||||
|
|
@ -59,12 +86,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
end
|
||||
|
||||
it 'passes database_replica_location' do
|
||||
expect(middleware).to receive_message_chain(:load_balancer, :host, "database_replica_location").and_return(location)
|
||||
expect(load_balancer).to receive_message_chain(:host, "database_replica_location").and_return(location)
|
||||
|
||||
middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
|
||||
run_middleware
|
||||
|
||||
expect(job['database_replica_location']).to eq(location)
|
||||
end
|
||||
|
||||
include_examples 'job data consistency'
|
||||
end
|
||||
|
||||
context 'when write was performed' do
|
||||
|
|
@ -73,12 +102,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
end
|
||||
|
||||
it 'passes primary write location', :aggregate_failures do
|
||||
expect(middleware).to receive_message_chain(:load_balancer, :primary_write_location).and_return(location)
|
||||
expect(load_balancer).to receive(:primary_write_location).and_return(location)
|
||||
|
||||
middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
|
||||
run_middleware
|
||||
|
||||
expect(job['database_write_location']).to eq(location)
|
||||
end
|
||||
|
||||
include_examples 'job data consistency'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -89,7 +120,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
end
|
||||
|
||||
it 'does not set database locations again' do
|
||||
middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
|
||||
run_middleware
|
||||
|
||||
expect(job[provided_database_location]).to eq(old_location)
|
||||
expect(job[other_location]).to be_nil
|
||||
|
|
@ -101,8 +132,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", provided_database_location => old_location } }
|
||||
|
||||
before do
|
||||
allow(middleware).to receive_message_chain(:load_balancer, :primary_write_location).and_return(new_location)
|
||||
allow(middleware).to receive_message_chain(:load_balancer, :database_replica_location).and_return(new_location)
|
||||
allow(load_balancer).to receive(:primary_write_location).and_return(new_location)
|
||||
allow(load_balancer).to receive(:database_replica_location).and_return(new_location)
|
||||
end
|
||||
|
||||
context "when write was performed" do
|
||||
|
|
@ -114,24 +145,16 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
|
|||
end
|
||||
end
|
||||
|
||||
let(:queue) { 'default' }
|
||||
let(:redis_pool) { Sidekiq.redis_pool }
|
||||
let(:worker_class) { 'TestDataConsistencyWorker' }
|
||||
let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e" } }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
skip_default_enabled_yaml_check
|
||||
end
|
||||
|
||||
context 'when worker cannot be constantized' do
|
||||
let(:worker_class) { 'ActionMailer::MailDeliveryJob' }
|
||||
let(:expected_consistency) { :always }
|
||||
|
||||
include_examples 'does not pass database locations'
|
||||
end
|
||||
|
||||
context 'when worker class does not include ApplicationWorker' do
|
||||
let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
|
||||
let(:expected_consistency) { :always }
|
||||
|
||||
include_examples 'does not pass database locations'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,11 +6,16 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
let(:middleware) { described_class.new }
|
||||
|
||||
let(:load_balancer) { double.as_null_object }
|
||||
let(:has_replication_lag) { false }
|
||||
|
||||
let(:worker) { worker_class.new }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8' } }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
skip_default_enabled_yaml_check
|
||||
allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer).and_return(load_balancer)
|
||||
allow(load_balancer).to receive(:select_up_to_date_host).and_return(!has_replication_lag)
|
||||
|
||||
replication_lag!(false)
|
||||
end
|
||||
|
||||
after do
|
||||
|
|
@ -39,24 +44,34 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'stick to the primary' do
|
||||
it 'sticks to the primary' do
|
||||
middleware.call(worker, job, double(:queue)) do
|
||||
expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).to be_truthy
|
||||
shared_examples_for 'load balancing strategy' do |strategy|
|
||||
it "sets load balancing strategy to #{strategy}" do
|
||||
run_middleware do
|
||||
expect(job['load_balancing_strategy']).to eq(strategy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'replica is up to date' do |location|
|
||||
shared_examples_for 'stick to the primary' do |expected_strategy|
|
||||
it 'sticks to the primary' do
|
||||
run_middleware do
|
||||
expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
include_examples 'load balancing strategy', expected_strategy
|
||||
end
|
||||
|
||||
shared_examples_for 'replica is up to date' do |location, expected_strategy|
|
||||
it 'does not stick to the primary', :aggregate_failures do
|
||||
expect(middleware).to receive(:replica_caught_up?).with(location).and_return(true)
|
||||
|
||||
middleware.call(worker, job, double(:queue)) do
|
||||
run_middleware do
|
||||
expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).not_to be_truthy
|
||||
end
|
||||
|
||||
expect(job['load_balancing_strategy']).to eq('replica')
|
||||
end
|
||||
|
||||
include_examples 'load balancing strategy', expected_strategy
|
||||
end
|
||||
|
||||
shared_examples_for 'sticks based on data consistency' do |data_consistency|
|
||||
|
|
@ -67,7 +82,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
stub_feature_flags(load_balancing_for_test_data_consistency_worker: false)
|
||||
end
|
||||
|
||||
include_examples 'stick to the primary'
|
||||
include_examples 'stick to the primary', 'primary'
|
||||
end
|
||||
|
||||
context 'when database replica location is set' do
|
||||
|
|
@ -77,7 +92,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
allow(middleware).to receive(:replica_caught_up?).and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'replica is up to date', '0/D525E3A8'
|
||||
it_behaves_like 'replica is up to date', '0/D525E3A8', 'replica'
|
||||
end
|
||||
|
||||
context 'when database primary location is set' do
|
||||
|
|
@ -87,46 +102,35 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
allow(middleware).to receive(:replica_caught_up?).and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'replica is up to date', '0/D525E3A8'
|
||||
it_behaves_like 'replica is up to date', '0/D525E3A8', 'replica'
|
||||
end
|
||||
|
||||
context 'when database location is not set' do
|
||||
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e' } }
|
||||
|
||||
it_behaves_like 'stick to the primary'
|
||||
it_behaves_like 'stick to the primary', 'primary_no_wal'
|
||||
end
|
||||
end
|
||||
|
||||
let(:queue) { 'default' }
|
||||
let(:redis_pool) { Sidekiq.redis_pool }
|
||||
let(:worker) { worker_class.new }
|
||||
let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8' } }
|
||||
let(:block) { 10 }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
skip_default_enabled_yaml_check
|
||||
allow(middleware).to receive(:clear)
|
||||
allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:performed_write?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when worker class does not include ApplicationWorker' do
|
||||
let(:worker) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.new }
|
||||
|
||||
include_examples 'stick to the primary'
|
||||
include_examples 'stick to the primary', 'primary'
|
||||
end
|
||||
|
||||
context 'when worker data consistency is :always' do
|
||||
include_context 'data consistency worker class', :always, :load_balancing_for_test_data_consistency_worker
|
||||
|
||||
include_examples 'stick to the primary'
|
||||
include_examples 'stick to the primary', 'primary'
|
||||
end
|
||||
|
||||
context 'when worker data consistency is :delayed' do
|
||||
include_examples 'sticks based on data consistency', :delayed
|
||||
|
||||
context 'when replica is not up to date' do
|
||||
let(:has_replication_lag) { true }
|
||||
before do
|
||||
replication_lag!(true)
|
||||
end
|
||||
|
||||
around do |example|
|
||||
with_sidekiq_server_middleware do |chain|
|
||||
|
|
@ -136,24 +140,34 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
end
|
||||
|
||||
context 'when job is executed first' do
|
||||
it 'raise an error and retries', :aggregate_failures do
|
||||
it 'raises an error and retries', :aggregate_failures do
|
||||
expect do
|
||||
process_job(job)
|
||||
end.to raise_error(Sidekiq::JobRetry::Skip)
|
||||
|
||||
expect(job['error_class']).to eq('Gitlab::Database::LoadBalancing::SidekiqServerMiddleware::JobReplicaNotUpToDate')
|
||||
expect(job['load_balancing_strategy']).to eq('retry_replica')
|
||||
end
|
||||
|
||||
include_examples 'load balancing strategy', 'retry'
|
||||
end
|
||||
|
||||
context 'when job is retried' do
|
||||
it 'stick to the primary', :aggregate_failures do
|
||||
before do
|
||||
expect do
|
||||
process_job(job)
|
||||
end.to raise_error(Sidekiq::JobRetry::Skip)
|
||||
end
|
||||
|
||||
process_job(job)
|
||||
expect(job['load_balancing_strategy']).to eq('retry_primary')
|
||||
context 'and replica still lagging behind' do
|
||||
include_examples 'stick to the primary', 'primary'
|
||||
end
|
||||
|
||||
context 'and replica is now up-to-date' do
|
||||
before do
|
||||
replication_lag!(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'replica is up to date', '0/D525E3A8', 'replica_retried'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -167,20 +181,24 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
|
|||
allow(middleware).to receive(:replica_caught_up?).and_return(false)
|
||||
end
|
||||
|
||||
include_examples 'stick to the primary'
|
||||
|
||||
it 'updates job hash with primary database chosen', :aggregate_failures do
|
||||
middleware.call(worker, job, double(:queue)) do
|
||||
expect(job['load_balancing_strategy']).to eq('primary')
|
||||
end
|
||||
end
|
||||
include_examples 'stick to the primary', 'primary'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_job(job)
|
||||
Sidekiq::JobRetry.new.local(worker_class, job, queue) do
|
||||
Sidekiq::JobRetry.new.local(worker_class, job, 'default') do
|
||||
worker_class.process_job(job)
|
||||
end
|
||||
end
|
||||
|
||||
def run_middleware
|
||||
middleware.call(worker, job, double(:queue)) { yield }
|
||||
rescue described_class::JobReplicaNotUpToDate
|
||||
# we silence errors here that cause the job to retry
|
||||
end
|
||||
|
||||
def replication_lag!(exists)
|
||||
allow(load_balancer).to receive(:select_up_to_date_host).and_return(!exists)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
|
|||
context 'when worker declares data consistency' do
|
||||
include_context 'worker declaring data consistency'
|
||||
|
||||
it 'increments load balancing counter' do
|
||||
it 'increments load balancing counter with defined data consistency' do
|
||||
process_job
|
||||
|
||||
expect(load_balancing_metric).to have_received(:increment).with(
|
||||
|
|
@ -272,10 +272,14 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
|
|||
end
|
||||
|
||||
context 'when worker does not declare data consistency' do
|
||||
it 'does not increment load balancing counter' do
|
||||
it 'increments load balancing counter with default data consistency' do
|
||||
process_job
|
||||
|
||||
expect(load_balancing_metric).not_to have_received(:increment)
|
||||
expect(load_balancing_metric).to have_received(:increment).with(
|
||||
a_hash_including(
|
||||
data_consistency: :always,
|
||||
load_balancing_strategy: 'primary'
|
||||
), 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1051,23 +1051,53 @@ RSpec.describe Issue do
|
|||
|
||||
describe '#check_for_spam?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
let_it_be(:support_bot) { ::User.support_bot }
|
||||
|
||||
where(:visibility_level, :confidential, :new_attributes, :check_for_spam?) do
|
||||
Gitlab::VisibilityLevel::PUBLIC | false | { description: 'woo' } | true
|
||||
Gitlab::VisibilityLevel::PUBLIC | false | { title: 'woo' } | true
|
||||
Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | true
|
||||
Gitlab::VisibilityLevel::PUBLIC | true | { description: 'woo' } | false
|
||||
Gitlab::VisibilityLevel::PUBLIC | false | { title: 'woo', confidential: true } | false
|
||||
Gitlab::VisibilityLevel::PUBLIC | false | { description: 'original description' } | false
|
||||
Gitlab::VisibilityLevel::INTERNAL | false | { description: 'woo' } | false
|
||||
Gitlab::VisibilityLevel::PRIVATE | false | { description: 'woo' } | false
|
||||
where(:support_bot?, :visibility_level, :confidential, :new_attributes, :check_for_spam?) do
|
||||
### non-support-bot cases
|
||||
# spammable attributes changing
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | false | { description: 'new' } | true
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | false | { title: 'new' } | true
|
||||
# confidential to non-confidential
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | true
|
||||
# non-confidential to confidential
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | false | { confidential: true } | false
|
||||
# spammable attributes changing on confidential
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | true | { description: 'new' } | false
|
||||
# spammable attributes changing while changing to confidential
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | false | { title: 'new', confidential: true } | false
|
||||
# spammable attribute not changing
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | false | { description: 'original description' } | false
|
||||
# non-spammable attribute changing
|
||||
false | Gitlab::VisibilityLevel::PUBLIC | false | { weight: 3 } | false
|
||||
# spammable attributes changing on non-public
|
||||
false | Gitlab::VisibilityLevel::INTERNAL | false | { description: 'new' } | false
|
||||
false | Gitlab::VisibilityLevel::PRIVATE | false | { description: 'new' } | false
|
||||
|
||||
### support-bot cases
|
||||
# confidential to non-confidential
|
||||
true | Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | true
|
||||
# non-confidential to confidential
|
||||
true | Gitlab::VisibilityLevel::PUBLIC | false | { confidential: true } | false
|
||||
# spammable attributes changing on confidential
|
||||
true | Gitlab::VisibilityLevel::PUBLIC | true | { description: 'new' } | true
|
||||
# spammable attributes changing while changing to confidential
|
||||
true | Gitlab::VisibilityLevel::PUBLIC | false | { title: 'new', confidential: true } | true
|
||||
# spammable attributes changing on non-public
|
||||
true | Gitlab::VisibilityLevel::INTERNAL | false | { description: 'new' } | true
|
||||
true | Gitlab::VisibilityLevel::PRIVATE | false | { title: 'new' } | true
|
||||
# spammable attribute not changing
|
||||
true | Gitlab::VisibilityLevel::PUBLIC | false | { description: 'original description' } | false
|
||||
# non-spammable attribute changing
|
||||
true | Gitlab::VisibilityLevel::PRIVATE | true | { weight: 3 } | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'checks for spam on issues that can be seen anonymously' do
|
||||
it 'checks for spam when necessary' do
|
||||
author = support_bot? ? support_bot : user
|
||||
project = reusable_project
|
||||
project.update!(visibility_level: visibility_level)
|
||||
issue = create(:issue, project: project, confidential: confidential, description: 'original description')
|
||||
issue = create(:issue, project: project, confidential: confidential, description: 'original description', author: author)
|
||||
|
||||
issue.assign_attributes(new_attributes)
|
||||
|
||||
|
|
|
|||
|
|
@ -441,6 +441,22 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.join_metrics' do
|
||||
let_it_be(:join_condition) { '"merge_request_metrics"."target_project_id" = 1' }
|
||||
|
||||
context 'when a no target_project_id is available' do
|
||||
it 'moves target_project_id condition to the merge request metrics' do
|
||||
expect(described_class.join_metrics(1).to_sql).to include(join_condition)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a target_project_id is present in the where conditions' do
|
||||
it 'moves target_project_id condition to the merge request metrics' do
|
||||
expect(described_class.where(target_project_id: 1).join_metrics.to_sql).to include(join_condition)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.by_related_commit_sha' do
|
||||
subject { described_class.by_related_commit_sha(sha) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'search timeouts' do |scope|
|
||||
context 'when search times out' do
|
||||
before do
|
||||
allow_next_instance_of(SearchService) do |service|
|
||||
allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled)
|
||||
end
|
||||
|
||||
visit(search_path(search: 'test', scope: scope))
|
||||
end
|
||||
|
||||
it 'renders timeout information' do
|
||||
expect(page).to have_content('Your search timed out')
|
||||
end
|
||||
|
||||
it 'sets tab count to 0' do
|
||||
expect(page.find('.search-filter .active')).to have_text('0')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -10,7 +10,7 @@ RSpec.describe Tooling::Danger::FeatureFlag do
|
|||
|
||||
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
|
||||
|
||||
subject(:feature_flag) { fake_danger.new(git: fake_git) }
|
||||
subject(:feature_flag) { fake_danger.new(helper: fake_helper) }
|
||||
|
||||
describe '#feature_flag_files' do
|
||||
let(:feature_flag_files) do
|
||||
|
|
|
|||
|
|
@ -413,20 +413,30 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
disabled_repository.project.container_expiration_policy.update_column(:enabled, false)
|
||||
end
|
||||
|
||||
context 'counts and capacity' do
|
||||
where(:scheduled_count, :unfinished_count, :capacity, :expected_count) do
|
||||
2 | 2 | 10 | 4
|
||||
2 | 0 | 10 | 2
|
||||
0 | 2 | 10 | 2
|
||||
4 | 2 | 2 | 4
|
||||
4 | 0 | 2 | 4
|
||||
0 | 4 | 2 | 4
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(worker).to receive(:cleanup_scheduled_count).and_return(scheduled_count)
|
||||
allow(worker).to receive(:cleanup_unfinished_count).and_return(unfinished_count)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(expected_count) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with container repositories waiting for cleanup' do
|
||||
let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
|
||||
|
||||
it { is_expected.to eq(3) }
|
||||
|
||||
it 'logs the work count' do
|
||||
expect_log_info(
|
||||
cleanup_scheduled_count: 1,
|
||||
cleanup_unfinished_count: 2,
|
||||
cleanup_total_count: 3
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no container repositories waiting for cleanup' do
|
||||
|
|
@ -436,16 +446,6 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
end
|
||||
|
||||
it { is_expected.to eq(0) }
|
||||
|
||||
it 'logs 0 work count' do
|
||||
expect_log_info(
|
||||
cleanup_scheduled_count: 0,
|
||||
cleanup_unfinished_count: 0,
|
||||
cleanup_total_count: 0
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -468,9 +468,4 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
it { is_expected.to eq(0) }
|
||||
end
|
||||
end
|
||||
|
||||
def expect_log_info(structure)
|
||||
expect(worker.logger)
|
||||
.to receive(:info).with(worker.structured_payload(structure))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@ RSpec.describe ContainerExpirationPolicyWorker do
|
|||
|
||||
context 'process stale ongoing cleanups' do
|
||||
let_it_be(:stuck_cleanup) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
|
||||
let_it_be(:container_repository) { create(:container_repository, :cleanup_scheduled) }
|
||||
let_it_be(:container_repository) { create(:container_repository, :cleanup_unfinished) }
|
||||
let_it_be(:container_repository1) { create(:container_repository, :cleanup_scheduled) }
|
||||
let_it_be(:container_repository2) { create(:container_repository, :cleanup_unfinished) }
|
||||
|
||||
it 'set them as unfinished' do
|
||||
expect { subject }
|
||||
|
|
@ -137,5 +137,36 @@ RSpec.describe ContainerExpirationPolicyWorker do
|
|||
expect(container_expiration_policy3.reload.enabled).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'counts logging' do
|
||||
let_it_be(:container_repository1) { create(:container_repository, :cleanup_scheduled) }
|
||||
let_it_be(:container_repository2) { create(:container_repository, :cleanup_unfinished) }
|
||||
let_it_be(:container_repository3) { create(:container_repository, :cleanup_unfinished) }
|
||||
|
||||
before do
|
||||
ContainerExpirationPolicy.update_all(enabled: true)
|
||||
container_repository1.project.container_expiration_policy.update_column(:next_run_at, 5.minutes.ago)
|
||||
end
|
||||
|
||||
it 'logs all the counts' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_required_count, 1)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_unfinished_count, 2)
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_total_count, 3)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'with load balancing enabled' do
|
||||
before do
|
||||
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
|
||||
end
|
||||
|
||||
it 'reads the counts from the replica' do
|
||||
expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_replicas_for_read_queries).and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ module Tooling
|
|||
# - :modified
|
||||
# - :deleted
|
||||
def feature_flag_files(change_type:)
|
||||
files = git.public_send("#{change_type}_files") # rubocop:disable GitlabSecurity/PublicSend
|
||||
files = helper.public_send("#{change_type}_files") # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
files.select { |path| path =~ %r{\A(ee/)?config/feature_flags/} }.map { |path| Found.new(path) }
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue