Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-09 12:08:17 +00:00
parent 1613500bf7
commit a373ecffca
70 changed files with 677 additions and 434 deletions

View File

@ -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:

View File

@ -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

View File

@ -1 +1 @@
fef798978197809e268e5395838b4f4eeb4288e9
49893735ed64feebc208f4efe90bebbb8bbb02ad

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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,

View File

@ -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"

View File

@ -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"

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1 @@
export const USER_COLLAPSED_GUTTER_COOKIE = 'collapsed_gutter';

View File

@ -1,5 +1,9 @@
mutation updateIssue($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issuable: issue {
id
state
}
errors
}
}

View File

@ -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">

View File

@ -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);

View File

@ -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,

View File

@ -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.',
);

View File

@ -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

View File

@ -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();

View File

@ -95,7 +95,6 @@ export default {
:name="folderIconName"
:size="size"
class="folder-icon"
use-deprecated-sizes
data-qa-selector="folder_icon_content"
/>
</span>

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1 @@
c33dd2c63d5a8c6e3c2f49e640b1780734b4bfca88378fac67ea5f5bd24fb2b4

View File

@ -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);

View File

@ -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

View File

@ -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"];

View File

@ -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,

View File

@ -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

View File

@ -17,9 +17,9 @@ and to-do items are assigned to you:
![issues and MRs dashboard links](img/dashboard_links_v13_11.png)
- **(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:

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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') }

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) => {

View File

@ -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);
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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