diff options
70 files changed, 669 insertions, 426 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 0b6675ab84f..b7a4ef45240 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -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: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 55ebd3ba038..b9cf5a7d5bd 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -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 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index af8db279ebd..28c55f889ba 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -fef798978197809e268e5395838b4f4eeb4288e9 +49893735ed64feebc208f4efe90bebbb8bbb02ad @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 9237261d7c1..44422535cc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue index 99fe3938046..cb441a7e491 100644 --- a/app/assets/javascripts/blob/components/blob_header_filepath.vue +++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue @@ -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" diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 60cfb758e14..8d88b682df2 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -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, diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 25d35b18fae..b7fbaaecb1d 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -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" diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue index dd320676e98..68b4438831e 100644 --- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue +++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue @@ -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" diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index a87d4f077cc..51b5237a339 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -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(); diff --git a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue index ca057094868..011db52cbe3 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue @@ -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> diff --git a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue index 8a159139af0..99dcccd12ed 100644 --- a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue +++ b/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue @@ -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> diff --git a/app/assets/javascripts/issuable_sidebar/constants.js b/app/assets/javascripts/issuable_sidebar/constants.js new file mode 100644 index 00000000000..4f4b6341a1c --- /dev/null +++ b/app/assets/javascripts/issuable_sidebar/constants.js @@ -0,0 +1 @@ +export const USER_COLLAPSED_GUTTER_COOKIE = 'collapsed_gutter'; diff --git a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql b/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql index 9c28fdded21..ec8d8f32d8b 100644 --- a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql +++ b/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql @@ -1,5 +1,9 @@ mutation updateIssue($input: UpdateIssueInput!) { updateIssue(input: $input) { + issuable: issue { + id + state + } errors } } diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue index 3e31e2e93ae..5fcc778a714 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue @@ -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"> diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index feaf8b0d996..0ddb2c2334c 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -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); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 602d6ef611f..c3c3aacae35 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -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, diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index ea2053b3326..874d51a3577 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -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.', ); diff --git a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql index b45b6b46c8f..28a47735143 100644 --- a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql +++ b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql @@ -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 diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 7ab8da93a57..2d0b7fe46a6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -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(); diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 2e7c10636a2..276fb35b51f 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -95,7 +95,6 @@ export default { :name="folderIconName" :size="size" class="folder-icon" - use-deprecated-sizes data-qa-selector="folder_icon_content" /> </span> diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index ac6239615b4..4160b528301 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -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') diff --git a/app/finders/concerns/merged_at_filter.rb b/app/finders/concerns/merged_at_filter.rb index e44354f36d1..581bcca3c25 100644 --- a/app/finders/concerns/merged_at_filter.rb +++ b/app/finders/concerns/merged_at_filter.rb @@ -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 diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c40feb42eea..d8ba530f3f6 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -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 diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 04cc6886eb1..1cbde1871d4 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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) } diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 9a0c82ddbb5..da32dfb0b9b 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -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' diff --git a/app/helpers/sorting_titles_values_helper.rb b/app/helpers/sorting_titles_values_helper.rb index b1921983104..9b839f4e9bc 100644 --- a/app/helpers/sorting_titles_values_helper.rb +++ b/app/helpers/sorting_titles_values_helper.rb @@ -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 diff --git a/app/models/issue.rb b/app/models/issue.rb index 5ed8a119035..3b236620ed6 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index fcbf1202e69..06512d2fdb9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -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 diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 4ba906dd02f..d5d3cd753f3 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -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 diff --git a/app/views/search/results/_timeout.html.haml b/app/views/search/results/_timeout.html.haml new file mode 100644 index 00000000000..740e2bedd54 --- /dev/null +++ b/app/views/search/results/_timeout.html.haml @@ -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 } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 5156ad96684..82f03421799 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -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 diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml index 9e3caf62d77..caf271e9ee9 100644 --- a/app/views/shared/issuable/_sort_dropdown.html.haml +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -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) diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb index 3027d46b8b1..33dda6a8f0c 100644 --- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb +++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb @@ -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 diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb index 8fc139ac87c..a35ca5d184e 100644 --- a/app/workers/container_expiration_policy_worker.rb +++ b/app/workers/container_expiration_policy_worker.rb @@ -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) diff --git a/db/migrate/20210705130919_create_container_repos_on_exp_cleanup_status_project_id_start_date_index.rb b/db/migrate/20210705130919_create_container_repos_on_exp_cleanup_status_project_id_start_date_index.rb new file mode 100644 index 00000000000..a6983c2d599 --- /dev/null +++ b/db/migrate/20210705130919_create_container_repos_on_exp_cleanup_status_project_id_start_date_index.rb @@ -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 diff --git a/db/schema_migrations/20210705130919 b/db/schema_migrations/20210705130919 new file mode 100644 index 00000000000..9e0b9ffe69a --- /dev/null +++ b/db/schema_migrations/20210705130919 @@ -0,0 +1 @@ +c33dd2c63d5a8c6e3c2f49e640b1780734b4bfca88378fac67ea5f5bd24fb2b4
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 2861a545945..21dd89ceedc 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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); diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 000c909664a..20530d805ca 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -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 diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index dd51d981658..b58e644d639 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -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"]; diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index dc23765337b..046dd125cd1 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -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, diff --git a/doc/user/admin_area/custom_project_templates.md b/doc/user/admin_area/custom_project_templates.md index 6cf3c5bbd7d..12d143b3a13 100644 --- a/doc/user/admin_area/custom_project_templates.md +++ b/doc/user/admin_area/custom_project_templates.md @@ -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 diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 0cdaa3150c5..babe13969ce 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -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: diff --git a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb index 524d69c00c0..0e36ebbc3ee 100644 --- a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb +++ b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb @@ -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 diff --git a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb index 4e06a94557d..0551750568a 100644 --- a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb +++ b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d4a9ce18a09..961b0da9f2a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 3ffdeb31894..a799f8cd925 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -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 diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 6b8dcd7dbb6..ab6242784fe 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -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) diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 56e93e4408b..f55dccce4f0 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -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') diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb index 2a12b22b457..5185a2460dc 100644 --- a/spec/features/search/user_searches_for_comments_spec.rb +++ b/spec/features/search/user_searches_for_comments_spec.rb @@ -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') } diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb index 1a882050126..279db686aa9 100644 --- a/spec/features/search/user_searches_for_commits_spec.rb +++ b/spec/features/search/user_searches_for_commits_spec.rb @@ -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) diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index 184f8ba0d36..b0902096770 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -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) diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index 32952a127d3..d7f490ba9bc 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -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) diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index e81abb44ba5..7a1ec16385c 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -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) diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb index e34ae031679..c38ad077cd0 100644 --- a/spec/features/search/user_searches_for_projects_spec.rb +++ b/spec/features/search/user_searches_for_projects_spec.rb @@ -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) diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb index 826ed73c9bf..a5cf12fa068 100644 --- a/spec/features/search/user_searches_for_users_spec.rb +++ b/spec/features/search/user_searches_for_users_spec.rb @@ -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 diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 8913f1fe9ee..06545d8640f 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -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 diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap index 53815820bbe..dfa6b99080b 100644 --- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap +++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap @@ -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 diff --git a/spec/frontend/issuable_show/components/issuable_show_root_spec.js b/spec/frontend/issuable_show/components/issuable_show_root_spec.js index b4c125f4910..7ad409c3a74 100644 --- a/spec/frontend/issuable_show/components/issuable_show_root_spec.js +++ b/spec/frontend/issuable_show/components/issuable_show_root_spec.js @@ -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) => { diff --git a/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js index 62a0016d67b..c872925cca2 100644 --- a/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js +++ b/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js @@ -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, - }); - - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('sidebar-toggle')).toBeTruthy(); - expect(wrapper.emitted('sidebar-toggle')[0]).toEqual([ - { - expanded: 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); - describe('methods', () => { - describe('updatePageContainerClass', () => { - beforeEach(() => { - setFixtures('<div class="layout-page"></div>'); - }); + wrapper = createComponent(); + }); - 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, - }); + 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 }); + }); - wrapper.vm.updatePageContainerClass(); + it('renders sidebar toggle button with text and icon', () => { + const buttonEl = findToggleSidebarButton(); - expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe( - true, - ); - }, - ); + 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('handleWindowResize', () => { - beforeEach(async () => { - wrapper.setData({ - userExpanded: true, - }); + describe('when collapsing the sidebar', () => { + it('updates "collapsed_gutter" cookie value and layout classes', async () => { + await findToggleSidebarButton().trigger('click'); - await wrapper.vm.$nextTick(); + 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`', () => { + expect(wrapper.classes()).toContain('right-sidebar-collapsed'); + }); - it('renders component container element with class `right-sidebar-collapsed` when `isExpanded` prop is false', () => { - expect(wrapper.classes()).toContain('right-sidebar-collapsed'); - }); + it('sets layout class to reflect collapsed state', () => { + assertPageLayoutClasses({ isExpanded: false }); + }); - it('renders sidebar toggle button with text and icon', () => { - const buttonEl = wrapper.find('button'); + 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('[data-testid="icon-expand"]').isVisible()).toBe(true); - }); + expect(buttonEl.exists()).toBe(true); + expect(buttonEl.attributes('title')).toBe('Toggle sidebar'); + expect(wrapper.findByTestId('icon-expand').isVisible()).toBe(true); }); + }); - it('renders sidebar items', () => { - const sidebarItemsEl = wrapper.find('[data-testid="sidebar-items"]'); + it('renders slotted sidebar items', () => { + wrapper = createComponent(); - expect(sidebarItemsEl.exists()).toBe(true); - expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true); - }); + const sidebarItemsEl = wrapper.findByTestId('sidebar-items'); + + expect(sidebarItemsEl.exists()).toBe(true); + expect(sidebarItemsEl.find('button.js-todo').exists()).toBe(true); }); }); diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb index 90051172fca..54050a87af0 100644 --- a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb @@ -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 diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb index a1e503147ed..14f240cd159 100644 --- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb @@ -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 + 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 'stick to the primary' do |expected_strategy| it 'sticks to the primary' do - middleware.call(worker, job, double(:queue)) 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| + 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 + + 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 - process_job(job) - expect(job['load_balancing_strategy']).to eq('retry_primary') + 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 diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index c4e0bc0df20..8a9767f9012 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -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 diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index edb93ecf4b6..1aa9019d240 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -1051,23 +1051,53 @@ RSpec.describe Issue do describe '#check_for_spam?' do using RSpec::Parameterized::TableSyntax - - 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 + let_it_be(:support_bot) { ::User.support_bot } + + 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) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2769a139c2a..554d2b0751d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -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) } diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb new file mode 100644 index 00000000000..bb5460e2a6f --- /dev/null +++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb @@ -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 diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb index 74e19d8f535..7cae3e0a8b3 100644 --- a/spec/tooling/danger/feature_flag_spec.rb +++ b/spec/tooling/danger/feature_flag_spec.rb @@ -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 diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb index c399697cbe0..506124216af 100644 --- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb +++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb @@ -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 diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb index 6f81d06f653..69ddbe5c0f4 100644 --- a/spec/workers/container_expiration_policy_worker_spec.rb +++ b/spec/workers/container_expiration_policy_worker_spec.rb @@ -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 diff --git a/tooling/danger/feature_flag.rb b/tooling/danger/feature_flag.rb index 06c89f83e23..cef64e52af3 100644 --- a/tooling/danger/feature_flag.rb +++ b/tooling/danger/feature_flag.rb @@ -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 |