diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-07 03:08:34 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-07 03:08:34 +0300 |
commit | 7e89568aa1b1c531aa34860fbd9e77d9e988b9b2 (patch) | |
tree | 9d644d947b75594d969f040ef046541c769e0dc3 | |
parent | f2143c9986ad7b6206b8a41cc9aeb419e543d3f5 (diff) |
Add latest changes from gitlab-org/gitlab@master
80 files changed, 1380 insertions, 283 deletions
diff --git a/.rubocop_todo/layout/first_hash_element_indentation.yml b/.rubocop_todo/layout/first_hash_element_indentation.yml index 34446f2cb61..2f20d2ec239 100644 --- a/.rubocop_todo/layout/first_hash_element_indentation.yml +++ b/.rubocop_todo/layout/first_hash_element_indentation.yml @@ -2,22 +2,6 @@ # Cop supports --autocorrect. Layout/FirstHashElementIndentation: Exclude: - - 'app/components/diffs/stats_component.rb' - - 'app/controllers/admin/ci/variables_controller.rb' - - 'app/controllers/admin/system_info_controller.rb' - - 'app/controllers/concerns/milestone_actions.rb' - - 'app/controllers/concerns/render_service_results.rb' - - 'app/controllers/concerns/sourcegraph_decorator.rb' - - 'app/controllers/profiles/two_factor_auths_controller.rb' - - 'app/controllers/projects/badges_controller.rb' - - 'app/controllers/repositories/lfs_locks_api_controller.rb' - - 'app/experiments/concerns/project_commit_count.rb' - - 'app/graphql/mutations/clusters/agent_tokens/create.rb' - - 'app/graphql/mutations/notes/create/diff_note.rb' - - 'app/graphql/mutations/notes/create/image_diff_note.rb' - - 'app/graphql/mutations/notes/create/note.rb' - - 'app/graphql/mutations/todos/restore_many.rb' - - 'app/graphql/resolvers/group_packages_resolver.rb' - 'app/helpers/avatars_helper.rb' - 'app/helpers/breadcrumbs_helper.rb' - 'app/helpers/broadcast_messages_helper.rb' diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue index 080f1fe222c..5e2bd096534 100644 --- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue +++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue @@ -5,9 +5,17 @@ import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue'; import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; import { IssuableStatus } from '~/issues/constants'; -import { PAGE_SIZE } from '~/issues/list/constants'; -import { getInitialPageParams } from '~/issues/list/utils'; +import { + CREATED_DESC, + PAGE_SIZE, + PARAM_STATE, + UPDATED_DESC, + urlSortParams, +} from '~/issues/list/constants'; +import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql'; +import { getInitialPageParams, getSortKey, getSortOptions, isSortKey } from '~/issues/list/utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; +import { getParameterByName } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; @@ -17,13 +25,10 @@ export default { calendarButtonText: __('Subscribe to calendar'), closed: __('CLOSED'), closedMoved: __('CLOSED (MOVED)'), - downvotes: __('Downvotes'), emptyStateTitle: __('Please select at least one filter to see results'), errorFetchingIssues: __('An error occurred while loading issues'), - relatedMergeRequests: __('Related merge requests'), rssButtonText: __('Subscribe to RSS feed'), searchInputPlaceholder: __('Search or filter results...'), - upvotes: __('Upvotes'), }, IssuableListTabs, components: { @@ -39,20 +44,35 @@ export default { inject: [ 'calendarPath', 'emptyStateSvgPath', + 'hasBlockedIssuesFeature', + 'hasIssuableHealthStatusFeature', + 'hasIssueWeightsFeature', 'hasScopedLabelsFeature', + 'initialSort', 'isPublicVisibilityRestricted', 'isSignedIn', 'rssPath', ], data() { + const state = getParameterByName(PARAM_STATE); + + const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC; + const dashboardSortKey = getSortKey(this.initialSort); + const graphQLSortKey = + isSortKey(this.initialSort?.toUpperCase()) && this.initialSort.toUpperCase(); + + // The initial sort is an old enum value when it is saved on the dashboard issues page. + // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page. + const sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey; + return { issues: [], issuesError: null, pageInfo: {}, pageParams: getInitialPageParams(), searchTokens: [], - sortOptions: [], - state: IssuableStates.Opened, + sortKey, + state: state || IssuableStates.Opened, }; }, apollo: { @@ -62,6 +82,7 @@ export default { return { hideUsers: this.isPublicVisibilityRestricted && !this.isSignedIn, isSignedIn: this.isSignedIn, + sort: this.sortKey, state: this.state, ...this.pageParams, }; @@ -82,6 +103,19 @@ export default { showPaginationControls() { return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage); }, + sortOptions() { + return getSortOptions({ + hasBlockedIssuesFeature: this.hasBlockedIssuesFeature, + hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature, + hasIssueWeightsFeature: this.hasIssueWeightsFeature, + }); + }, + urlParams() { + return { + sort: urlSortParams[this.sortKey], + state: this.state, + }; + }, }, methods: { getStatus(issue) { @@ -117,6 +151,33 @@ export default { }; scrollUp(); }, + handleSort(sortKey) { + if (this.sortKey === sortKey) { + return; + } + + this.pageParams = getInitialPageParams(); + this.sortKey = sortKey; + + if (this.isSignedIn) { + this.saveSortPreference(sortKey); + } + }, + saveSortPreference(sortKey) { + this.$apollo + .mutate({ + mutation: setSortPreferenceMutation, + variables: { input: { issuesSort: sortKey } }, + }) + .then(({ data }) => { + if (data.userPreferencesUpdate.errors.length) { + throw new Error(data.userPreferencesUpdate.errors); + } + }) + .catch((error) => { + Sentry.captureException(error); + }); + }, }, }; </script> @@ -128,6 +189,7 @@ export default { :has-next-page="pageInfo.hasNextPage" :has-previous-page="pageInfo.hasPreviousPage" :has-scoped-labels-feature="hasScopedLabelsFeature" + :initial-sort-by="sortKey" :issuables="issues" :issuables-loading="$apollo.queries.issues.loading" namespace="dashboard" @@ -137,11 +199,13 @@ export default { :show-pagination-controls="showPaginationControls" :sort-options="sortOptions" :tabs="$options.IssuableListTabs" + :url-params="urlParams" use-keyset-pagination @click-tab="handleClickTab" @dismiss-alert="handleDismissAlert" @next-page="handleNextPage" @previous-page="handlePreviousPage" + @sort="handleSort" > <template #nav-actions> <gl-button :href="rssPath" icon="rss"> diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js index ed11a600d4c..e3e5cc614cb 100644 --- a/app/assets/javascripts/issues/dashboard/index.js +++ b/app/assets/javascripts/issues/dashboard/index.js @@ -20,6 +20,7 @@ export function mountIssuesDashboardApp() { hasIssuableHealthStatusFeature, hasIssueWeightsFeature, hasScopedLabelsFeature, + initialSort, isPublicVisibilityRestricted, isSignedIn, rssPath, @@ -38,6 +39,7 @@ export function mountIssuesDashboardApp() { hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature), hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature), + initialSort, isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted), isSignedIn: parseBoolean(isSignedIn), rssPath, diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql index 2e70fb1eade..6d0c7139068 100644 --- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql +++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql @@ -4,6 +4,7 @@ query getDashboardIssues( $hideUsers: Boolean = false $isSignedIn: Boolean = false + $sort: IssueSort $state: IssuableState $afterCursor: String $beforeCursor: String @@ -11,6 +12,7 @@ query getDashboardIssues( $lastPageSize: Int ) { issues( + sort: $sort state: $state after: $afterCursor before: $beforeCursor diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 6e0782ac866..d661ce67d88 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -23,6 +23,7 @@ import { OPERATORS_IS_NOT, OPERATORS_IS_NOT_OR, OPTIONS_NONE_ANY, + TOKEN_TITLE_SEARCH_WITHIN, TOKEN_TITLE_ASSIGNEE, TOKEN_TITLE_AUTHOR, TOKEN_TITLE_CONFIDENTIAL, @@ -43,6 +44,7 @@ import { TOKEN_TYPE_ORGANIZATION, TOKEN_TYPE_RELEASE, TOKEN_TYPE_TYPE, + TOKEN_TYPE_SEARCH_WITHIN, } from '~/vue_shared/components/filtered_search_bar/constants'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; @@ -305,6 +307,22 @@ export default { const tokens = [ { + type: TOKEN_TYPE_SEARCH_WITHIN, + title: TOKEN_TITLE_SEARCH_WITHIN, + icon: 'search', + token: GlFilteredSearchToken, + unique: true, + operators: OPERATORS_IS, + options: [ + { icon: 'title', value: 'TITLE', title: this.$options.i18n.titles }, + { + icon: 'text-description', + value: 'DESCRIPTION', + title: this.$options.i18n.descriptions, + }, + ], + }, + { type: TOKEN_TYPE_AUTHOR, title: TOKEN_TITLE_AUTHOR, icon: 'pencil', diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index dc8adf9473f..683a5955465 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -22,6 +22,7 @@ import { TOKEN_TYPE_RELEASE, TOKEN_TYPE_TYPE, TOKEN_TYPE_WEIGHT, + TOKEN_TYPE_SEARCH_WITHIN, } from '~/vue_shared/components/filtered_search_bar/constants'; import { WORK_ITEM_TYPE_ENUM_INCIDENT, @@ -111,6 +112,8 @@ export const i18n = { rssLabel: __('Subscribe to RSS feed'), searchPlaceholder: __('Search or filter results...'), upvotes: __('Upvotes'), + titles: __('Titles'), + descriptions: __('Descriptions'), }; export const urlSortParams = { @@ -120,8 +123,8 @@ export const urlSortParams = { [CREATED_DESC]: 'created_date', [UPDATED_ASC]: 'updated_asc', [UPDATED_DESC]: 'updated_desc', - [CLOSED_AT_ASC]: 'closed_asc', - [CLOSED_AT_DESC]: 'closed_desc', + [CLOSED_AT_ASC]: 'closed_at', + [CLOSED_AT_DESC]: 'closed_at_desc', [MILESTONE_DUE_ASC]: 'milestone', [MILESTONE_DUE_DESC]: 'milestone_due_desc', [DUE_DATE_ASC]: 'due_date', @@ -189,6 +192,19 @@ export const filters = { }, }, }, + [TOKEN_TYPE_SEARCH_WITHIN]: { + [API_PARAM]: { + [NORMAL_FILTER]: 'in', + }, + [URL_PARAM]: { + [OPERATOR_IS]: { + [NORMAL_FILTER]: 'in', + }, + [OPERATOR_NOT]: { + [NORMAL_FILTER]: 'not[in]', + }, + }, + }, [TOKEN_TYPE_ASSIGNEE]: { [API_PARAM]: { [NORMAL_FILTER]: 'assigneeUsernames', diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql index b447289b425..ee97fb6edca 100644 --- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql +++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql @@ -10,6 +10,7 @@ query getIssues( $search: String $sort: IssueSort $state: IssuableState + $in: [IssuableSearchableField!] $assigneeId: String $assigneeUsernames: [String!] $authorUsername: String @@ -38,6 +39,7 @@ query getIssues( search: $search sort: $sort state: $state + in: $in assigneeId: $assigneeId assigneeUsernames: $assigneeUsernames authorUsername: $authorUsername @@ -72,6 +74,7 @@ query getIssues( search: $search sort: $sort state: $state + in: $in assigneeId: $assigneeId assigneeUsernames: $assigneeUsernames authorUsername: $authorUsername diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index f9f4e981863..993b4c11c0e 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -60,6 +60,7 @@ export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch'); export const TOKEN_TITLE_STATUS = __('Status'); export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch'); export const TOKEN_TITLE_TYPE = __('Type'); +export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within'); export const TOKEN_TYPE_APPROVED_BY = 'approved-by'; export const TOKEN_TYPE_ASSIGNEE = 'assignee'; @@ -84,3 +85,5 @@ export const TOKEN_TYPE_STATUS = 'status'; export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch'; export const TOKEN_TYPE_TYPE = 'type'; export const TOKEN_TYPE_WEIGHT = 'weight'; + +export const TOKEN_TYPE_SEARCH_WITHIN = 'in'; diff --git a/app/components/diffs/stats_component.rb b/app/components/diffs/stats_component.rb index dc42a849ccf..407c3ca4e58 100644 --- a/app/components/diffs/stats_component.rb +++ b/app/components/diffs/stats_component.rb @@ -14,14 +14,14 @@ module Diffs def diff_files_data diffs_map = @diff_files.map do |f| { - href: "##{helpers.hexdigest(f.file_path)}", - title: f.new_path, - name: f.file_path, - path: diff_file_path_text(f), - icon: diff_file_changed_icon(f), - iconColor: diff_file_changed_icon_color(f).to_s, - added: f.added_lines, - removed: f.removed_lines + href: "##{helpers.hexdigest(f.file_path)}", + title: f.new_path, + name: f.file_path, + path: diff_file_path_text(f), + icon: diff_file_changed_icon(f), + iconColor: diff_file_changed_icon_color(f).to_s, + added: f.added_lines, + removed: f.removed_lines } end diff --git a/app/controllers/admin/ci/variables_controller.rb b/app/controllers/admin/ci/variables_controller.rb index 02d551115d0..cd9bf422eee 100644 --- a/app/controllers/admin/ci/variables_controller.rb +++ b/app/controllers/admin/ci/variables_controller.rb @@ -32,8 +32,8 @@ class Admin::Ci::VariablesController < Admin::ApplicationController def render_instance_variables render status: :ok, json: { - variables: Ci::InstanceVariableSerializer.new.represent(variables) - } + variables: Ci::InstanceVariableSerializer.new.represent(variables) + } end def render_error(errors) diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb index 41f95addc66..96fb73cedfe 100644 --- a/app/controllers/admin/system_info_controller.rb +++ b/app/controllers/admin/system_info_controller.rb @@ -59,11 +59,11 @@ class Admin::SystemInfoController < Admin::ApplicationController begin disk = Sys::Filesystem.stat(mount.mount_point) @disks.push({ - bytes_total: disk.bytes_total, - bytes_used: disk.bytes_used, - disk_name: mount.name, - mount_path: disk.path - }) + bytes_total: disk.bytes_total, + bytes_used: disk.bytes_used, + disk_name: mount.name, + mount_path: disk.path + }) rescue Sys::Filesystem::Error end end diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index 0a859bd3af9..e1967c50d70 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -8,9 +8,9 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_issues_tab", { - issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables - show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name]) - }) + issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables + show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name]) + }) end end end @@ -20,9 +20,9 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { - merge_requests: @milestone.sorted_merge_requests(current_user).preload_milestoneish_associations, # rubocop:disable Gitlab/ModuleWithInstanceVariables - show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name]) - }) + merge_requests: @milestone.sorted_merge_requests(current_user).preload_milestoneish_associations, # rubocop:disable Gitlab/ModuleWithInstanceVariables + show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name]) + }) end end end @@ -32,8 +32,8 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_participants_tab", { - users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables - }) + users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables + }) end end end @@ -46,10 +46,10 @@ module MilestoneActions milestone_labels = @milestone.issue_labels_visible_by_user(current_user) render json: tabs_json("shared/milestones/_labels_tab", { - labels: milestone_labels.map do |label| - label.present(issuable_subject: @milestone.resource_parent) - end - }) + labels: milestone_labels.map do |label| + label.present(issuable_subject: @milestone.resource_parent) + end + }) end end end diff --git a/app/controllers/concerns/render_service_results.rb b/app/controllers/concerns/render_service_results.rb index 0149a71d9f5..83b880096be 100644 --- a/app/controllers/concerns/render_service_results.rb +++ b/app/controllers/concerns/render_service_results.rb @@ -5,25 +5,25 @@ module RenderServiceResults def success_response(result) render({ - status: result[:http_status], - json: result[:body] - }) + status: result[:http_status], + json: result[:body] + }) end def continue_polling_response render({ - status: :no_content, - json: { - status: _('processing'), - message: _('Not ready yet. Try again later.') - } - }) + status: :no_content, + json: { + status: _('processing'), + message: _('Not ready yet. Try again later.') + } + }) end def error_response(result) render({ - status: result[:http_status] || :bad_request, - json: { status: result[:status], message: result[:message] } - }) + status: result[:http_status] || :bad_request, + json: { status: result[:status], message: result[:message] } + }) end end diff --git a/app/controllers/concerns/sourcegraph_decorator.rb b/app/controllers/concerns/sourcegraph_decorator.rb index 061990a4361..4aeace1ca67 100644 --- a/app/controllers/concerns/sourcegraph_decorator.rb +++ b/app/controllers/concerns/sourcegraph_decorator.rb @@ -22,8 +22,8 @@ module SourcegraphDecorator return unless sourcegraph_enabled? gon.push({ - sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url } - }) + sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url } + }) end def sourcegraph_enabled? diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 9ef5cf0ed09..03b7cc9f892 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -186,9 +186,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def u2f_registrations current_user.u2f_registrations.map do |u2f_registration| { - name: u2f_registration.name, - created_at: u2f_registration.created_at, - delete_path: profile_u2f_registration_path(u2f_registration) + name: u2f_registration.name, + created_at: u2f_registration.created_at, + delete_path: profile_u2f_registration_path(u2f_registration) } end end @@ -196,9 +196,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def webauthn_registrations current_user.webauthn_registrations.map do |webauthn_registration| { - name: webauthn_registration.name, - created_at: webauthn_registration.created_at, - delete_path: profile_webauthn_registration_path(webauthn_registration) + name: webauthn_registration.name, + created_at: webauthn_registration.created_at, + delete_path: profile_webauthn_registration_path(webauthn_registration) } end end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 42bd87e1c01..dbbffc4c283 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -13,10 +13,10 @@ class Projects::BadgesController < Projects::ApplicationController def pipeline pipeline_status = Gitlab::Ci::Badge::Pipeline::Status .new(project, params[:ref], opts: { - ignore_skipped: params[:ignore_skipped], - key_text: params[:key_text], - key_width: params[:key_width] - }) + ignore_skipped: params[:ignore_skipped], + key_text: params[:key_text], + key_width: params[:key_width] + }) render_badge pipeline_status end @@ -24,13 +24,13 @@ class Projects::BadgesController < Projects::ApplicationController def coverage coverage_report = Gitlab::Ci::Badge::Coverage::Report .new(project, params[:ref], opts: { - job: params[:job], - key_text: params[:key_text], - key_width: params[:key_width], - min_good: params[:min_good], - min_acceptable: params[:min_acceptable], - min_medium: params[:min_medium] - }) + job: params[:job], + key_text: params[:key_text], + key_width: params[:key_width], + min_good: params[:min_good], + min_acceptable: params[:min_acceptable], + min_medium: params[:min_medium] + }) render_badge coverage_report end @@ -38,10 +38,10 @@ class Projects::BadgesController < Projects::ApplicationController def release latest_release = Gitlab::Ci::Badge::Release::LatestRelease .new(project, current_user, opts: { - key_text: params[:key_text], - key_width: params[:key_width], - order_by: params[:order_by] - }) + key_text: params[:key_text], + key_width: params[:key_width], + order_by: params[:order_by] + }) render_badge latest_release end diff --git a/app/controllers/repositories/lfs_locks_api_controller.rb b/app/controllers/repositories/lfs_locks_api_controller.rb index 0b765aa6931..ea858d63236 100644 --- a/app/controllers/repositories/lfs_locks_api_controller.rb +++ b/app/controllers/repositories/lfs_locks_api_controller.rb @@ -54,9 +54,9 @@ module Repositories def error_payload(message, custom_attrs = {}) custom_attrs.merge({ - message: message, - documentation_url: help_url - }) + message: message, + documentation_url: help_url + }) end def split_by_owner(locks) diff --git a/app/experiments/concerns/project_commit_count.rb b/app/experiments/concerns/project_commit_count.rb index 706a1a24640..3f08538c21f 100644 --- a/app/experiments/concerns/project_commit_count.rb +++ b/app/experiments/concerns/project_commit_count.rb @@ -10,9 +10,9 @@ module ProjectCommitCount return default_count unless root_ref Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(root_ref, { - all: true, # include all branches - max_count: max_count # limit as an optimization - }) + all: true, # include all branches + max_count: max_count # limit as an optimization + }) rescue StandardError => e Gitlab::ErrorTracking.track_exception(e, exception_details) diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb index a99a54fa5ed..c10e1633350 100644 --- a/app/graphql/mutations/clusters/agent_tokens/create.rb +++ b/app/graphql/mutations/clusters/agent_tokens/create.rb @@ -49,9 +49,9 @@ module Mutations payload = result.payload { - secret: payload[:secret], - token: payload[:token], - errors: Array.wrap(result.message) + secret: payload[:secret], + token: payload[:token], + errors: Array.wrap(result.message) } end diff --git a/app/graphql/mutations/notes/create/diff_note.rb b/app/graphql/mutations/notes/create/diff_note.rb index 7b8c06fd104..df2bd55106e 100644 --- a/app/graphql/mutations/notes/create/diff_note.rb +++ b/app/graphql/mutations/notes/create/diff_note.rb @@ -31,10 +31,10 @@ module Mutations def create_note_params(noteable, args) super(noteable, args).merge({ - type: 'DiffNote', - position: position(noteable, args), - merge_request_diff_head_sha: args[:position][:head_sha] - }) + type: 'DiffNote', + position: position(noteable, args), + merge_request_diff_head_sha: args[:position][:head_sha] + }) end def position(noteable, args) diff --git a/app/graphql/mutations/notes/create/image_diff_note.rb b/app/graphql/mutations/notes/create/image_diff_note.rb index d94fd4d6ff8..3de93e4f5c1 100644 --- a/app/graphql/mutations/notes/create/image_diff_note.rb +++ b/app/graphql/mutations/notes/create/image_diff_note.rb @@ -15,9 +15,9 @@ module Mutations def create_note_params(noteable, args) super(noteable, args).merge({ - type: 'DiffNote', - position: position(noteable, args) - }) + type: 'DiffNote', + position: position(noteable, args) + }) end def position(noteable, args) diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb index 4d6f056de09..9b105b7fe1c 100644 --- a/app/graphql/mutations/notes/create/note.rb +++ b/app/graphql/mutations/notes/create/note.rb @@ -31,9 +31,9 @@ module Mutations end super(noteable, args).merge({ - in_reply_to_discussion_id: discussion_id, - merge_request_diff_head_sha: args[:merge_request_diff_head_sha] - }) + in_reply_to_discussion_id: discussion_id, + merge_request_diff_head_sha: args[:merge_request_diff_head_sha] + }) end def authorize_discussion!(discussion) diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index 20913a9e7da..f2f944860c2 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -23,9 +23,9 @@ module Mutations updated_ids = restore(todos) { - updated_ids: updated_ids, - todos: Todo.id_in(updated_ids), - errors: errors_on_objects(todos) + updated_ids: updated_ids, + todos: Todo.id_in(updated_ids), + errors: errors_on_objects(todos) } end diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb index e6a6abb39dd..ae578390fd5 100644 --- a/app/graphql/resolvers/group_packages_resolver.rb +++ b/app/graphql/resolvers/group_packages_resolver.rb @@ -13,9 +13,9 @@ module Resolvers default_value: :created_desc GROUP_SORT_TO_PARAMS_MAP = SORT_TO_PARAMS_MAP.merge({ - project_path_desc: { order_by: 'project_path', sort: 'desc' }, - project_path_asc: { order_by: 'project_path', sort: 'asc' } - }).freeze + project_path_desc: { order_by: 'project_path', sort: 'desc' }, + project_path_asc: { order_by: 'project_path', sort: 'asc' } + }).freeze def resolve(sort:, **filters) return unless packages_available? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 9d404736dac..1d68dccc741 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -260,6 +260,7 @@ module IssuesHelper { calendar_path: url_for(safe_params.merge(calendar_url_options)), empty_state_svg_path: image_path('illustrations/issue-dashboard_results-without-filter.svg'), + initial_sort: current_user&.user_preference&.issues_sort, is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s, is_signed_in: current_user.present?.to_s, diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index cbfcb1807f0..7a2a91aa0d2 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -57,6 +57,7 @@ module Integrations label: Integration::SNOWPLOW_EVENT_LABEL, property: key, user: User.find(user_id), + context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context], **optional_arguments ) end diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index 7945e185e8c..45302a0bd09 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -404,6 +404,7 @@ module Integrations label: Integration::SNOWPLOW_EVENT_LABEL, property: key, user: user, + context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context], **optional_arguments ) end diff --git a/app/models/project.rb b/app/models/project.rb index 5d66e7d2854..bb266d86cdb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -265,10 +265,24 @@ class Project < ApplicationRecord has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus' has_many :incident_management_timeline_event_tags, inverse_of: :project, class_name: 'IncidentManagement::TimelineEventTag' has_many :labels, class_name: 'ProjectLabel' - has_many :integrations has_many :events has_many :milestones + has_many :integrations + has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration' + has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration' + has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration' + has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration' + has_many :deployment_hooks_integrations, -> { deployment_hooks }, class_name: 'Integration' + has_many :issue_hooks_integrations, -> { issue_hooks }, class_name: 'Integration' + has_many :job_hooks_integrations, -> { job_hooks }, class_name: 'Integration' + has_many :merge_request_hooks_integrations, -> { merge_request_hooks }, class_name: 'Integration' + has_many :note_hooks_integrations, -> { note_hooks }, class_name: 'Integration' + has_many :pipeline_hooks_integrations, -> { pipeline_hooks }, class_name: 'Integration' + has_many :push_hooks_integrations, -> { push_hooks }, class_name: 'Integration' + has_many :tag_push_hooks_integrations, -> { tag_push_hooks }, class_name: 'Integration' + has_many :wiki_page_hooks_integrations, -> { wiki_page_hooks }, class_name: 'Integration' + # Projects with a very large number of notes may time out destroying them # through the foreign key. Additionally, the deprecated attachment uploader # for notes requires us to use dependent: :destroy to avoid orphaning uploaded @@ -1713,8 +1727,14 @@ class Project < ApplicationRecord def execute_integrations(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope run_after_commit_or_now do - integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend - integration.async_execute(data) + if use_integration_relations? + association("#{hooks_scope}_integrations").reader.each do |integration| + integration.async_execute(data) + end + else + integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend + integration.async_execute(data) + end end end end @@ -3347,6 +3367,12 @@ class Project < ApplicationRecord ProjectFeature::PRIVATE end end + + def use_integration_relations? + strong_memoize(:use_integration_relations) do + Feature.enabled?(:cache_project_integrations, self) + end + end end Project.prepend_mod_with('Project') diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb index 47be692d57a..d26ce5465cd 100644 --- a/app/models/project_export_job.rb +++ b/app/models/project_export_job.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class ProjectExportJob < ApplicationRecord + include EachBatch + + EXPIRES_IN = 7.days + belongs_to :project has_many :relation_exports, class_name: 'Projects::ImportExport::RelationExport' @@ -13,6 +17,8 @@ class ProjectExportJob < ApplicationRecord failed: 3 }.freeze + scope :prunable, -> { where("updated_at < ?", EXPIRES_IN.ago) } + state_machine :status, initial: :queued do event :start do transition [:queued] => :started @@ -31,4 +37,12 @@ class ProjectExportJob < ApplicationRecord state :finished, value: STATUS[:finished] state :failed, value: STATUS[:failed] end + + class << self + def prune_expired_jobs + prunable.each_batch do |relation| # rubocop:disable Style/SymbolProc + relation.delete_all + end + end + end end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 41c924029d7..1ce866bd910 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -19,6 +19,14 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:deactivated) { @user&.deactivated? } + desc "User is bot" + with_options scope: :user, score: 0 + condition(:bot) { @user&.bot? } + + desc "User is alert bot" + with_options scope: :user, score: 0 + condition(:alert_bot) { @user&.alert_bot? } + desc "User is support bot" with_options scope: :user, score: 0 condition(:support_bot) { @user&.support_bot? } @@ -50,9 +58,6 @@ class BasePolicy < DeclarativePolicy::Base ::Gitlab::ExternalAuthorization.perform_check? end - with_options scope: :user, score: 0 - condition(:alert_bot) { @user&.alert_bot? } - rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do prevent :read_cross_project end @@ -68,8 +73,6 @@ class BasePolicy < DeclarativePolicy::Base rule { default }.enable :read_cross_project condition(:is_gitlab_com, score: 0, scope: :global) { ::Gitlab.com? } - - condition(:is_bot?) { @user&.bot? } end BasePolicy.prepend_mod_with('BasePolicy') diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index 62840b0129f..32128d84d0b 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -18,7 +18,7 @@ class MergeRequestPolicy < IssuablePolicy enable :approve_merge_request end - rule { can?(:approve_merge_request) & is_bot? }.policy do + rule { can?(:approve_merge_request) & bot }.policy do enable :reset_merge_request_approvals end diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 4edb0f324dc..8750b80ccfd 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,8 +1,7 @@ .nav-block.activities = render 'shared/event_filter' .controls - = link_to dashboard_projects_path(rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip', title: 'Subscribe' do - = sprite_icon('rss', css_class: 'gl-icon') + = render Pajamas::ButtonComponent.new(href: dashboard_projects_path(rss_url_options), icon: 'rss', button_options: { title: _('Subscribe'), aria: { label: _('Subscribe') }, class: 'gl-display-none gl-sm-display-inline-flex' }) .content_list .loading diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 9c492a0da34..a819a39af8f 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -9,7 +9,8 @@ - if current_user.can_create_project? .page-title-controls - = link_to _("New project"), new_project_path, class: "gl-button btn btn-confirm", data: { qa_selector: 'new_project_button' } + = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' } }) do + = _("New project") .top-area .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index be2124fdd7e..5a798c249d1 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -4,7 +4,8 @@ - if current_user && current_user.snippets.any? || @snippets.any? .page-title-controls - if can?(current_user, :create_snippet) - = link_to _("New snippet"), new_snippet_path, class: "gl-button btn btn-confirm", title: _("New snippet") + = render Pajamas::ButtonComponent.new(href: new_snippet_path, variant: :confirm, button_options: { title: _("New snippet") }) do + = _("New snippet") .top-area = gl_tabs_nav({ class: 'gl-border-0' }) do diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 17931152bd0..b3bda801fad 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -426,6 +426,15 @@ :weight: 1 :idempotent: false :tags: [] +- :name: cronjob:export_prune_project_export_jobs + :worker_name: Gitlab::Export::PruneProjectExportJobsWorker + :feature_category: :importers + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:gitlab_service_ping :worker_name: GitlabServicePingWorker :feature_category: :service_ping diff --git a/app/workers/container_registry/migration/enqueuer_worker.rb b/app/workers/container_registry/migration/enqueuer_worker.rb index 1dd29eff86e..a4f5ac8eb7e 100644 --- a/app/workers/container_registry/migration/enqueuer_worker.rb +++ b/app/workers/container_registry/migration/enqueuer_worker.rb @@ -130,13 +130,13 @@ module ContainerRegistry # this issue. # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87733 and # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90735 for details. - ContainerRepository.ready_for_import.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord + ContainerRepository.ready_for_import.ordered.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord end end def next_aborted_repository strong_memoize(:next_aborted_repository) do - ContainerRepository.with_migration_state('import_aborted').limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord + ContainerRepository.with_migration_state('import_aborted').ordered.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord end end diff --git a/app/workers/gitlab/export/prune_project_export_jobs_worker.rb b/app/workers/gitlab/export/prune_project_export_jobs_worker.rb new file mode 100644 index 00000000000..9a3c0c80f85 --- /dev/null +++ b/app/workers/gitlab/export/prune_project_export_jobs_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Export + class PruneProjectExportJobsWorker + include ApplicationWorker + + # rubocop:disable Scalability/CronWorkerContext + # This worker updates several import states inline and does not schedule + # other jobs. So no context needed + include CronjobQueue + # rubocop:enable Scalability/CronWorkerContext + + feature_category :importers + data_consistency :always + idempotent! + + def perform + ProjectExportJob.prune_expired_jobs + end + end + end +end diff --git a/bin/audit-event-type b/bin/audit-event-type index 8704dcfc0b0..fec34724c7c 100755 --- a/bin/audit-event-type +++ b/bin/audit-event-type @@ -36,7 +36,7 @@ class AuditEventTypeOptionParser Options = Struct.new( :name, :description, - :group, + :feature_category, :milestone, :saved_to_database, :streamed, @@ -71,9 +71,9 @@ class AuditEventTypeOptionParser options.description = value end - opts.on('-g', '--group [string]', String, -"Name of the group that introduced this audit event. For example, govern::compliance") do |value| - options.group = value + opts.on('-c', '--feature-category [string]', String, +"The feature category of this audit event. For example, compliance_management") do |value| + options.feature_category = value end opts.on('-M', '--milestone [string]', String, @@ -145,16 +145,16 @@ class AuditEventTypeOptionParser end end - def read_group + def read_feature_category $stdout.puts - $stdout.puts ">> Specify the group introducing the audit event type, like `govern::compliance`:" + $stdout.puts ">> Specify the feature category of this audit event, like `compliance_management`:" loop do - group = Readline.readline('?> ', false)&.strip - group = nil if group.empty? - return group unless group.nil? + feature_category = Readline.readline('?> ', false)&.strip + feature_category = nil if feature_category.empty? + return feature_category unless feature_category.nil? - warn "group is a required field." + warn "feature_category is a required field." end end @@ -231,7 +231,7 @@ class AuditEventTypeCreator assert_existing_audit_event_type! options.description ||= AuditEventTypeOptionParser.read_description - options.group ||= AuditEventTypeOptionParser.read_group + options.feature_category ||= AuditEventTypeOptionParser.read_feature_category options.milestone ||= AuditEventTypeOptionParser.read_milestone options.saved_to_database = AuditEventTypeOptionParser.read_saved_to_database if options.saved_to_database.nil? options.streamed = AuditEventTypeOptionParser.read_streamed if options.streamed.nil? @@ -263,7 +263,7 @@ class AuditEventTypeCreator { 'name' => options.name, 'description' => options.description, - 'group' => options.group, + 'feature_category' => options.feature_category, 'milestone' => options.milestone, 'saved_to_database' => options.saved_to_database, 'streamed' => options.streamed, diff --git a/config/audit_events/types/policy_project_updated.yml b/config/audit_events/types/policy_project_updated.yml index 6fffc7f6b10..cb69c2ab796 100644 --- a/config/audit_events/types/policy_project_updated.yml +++ b/config/audit_events/types/policy_project_updated.yml @@ -3,6 +3,6 @@ description: "This event is triggered whenever the security policy project is up introduced_by_issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/377877" introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154" milestone: "15.6" -group: "govern::security policies" +feature_category: "security_policy_management" saved_to_database: true streamed: false diff --git a/config/audit_events/types/type_schema.json b/config/audit_events/types/type_schema.json index 0d5d79bc4c4..3921b36ba91 100644 --- a/config/audit_events/types/type_schema.json +++ b/config/audit_events/types/type_schema.json @@ -28,9 +28,9 @@ "https" ] }, - "group": { + "feature_category": { "type": "string", - "description": "Name of the group that introduced this audit event. For example, manage::compliance" + "description": "The feature category of this audit event. For example, compliance_management" }, "milestone": { "type": "string", @@ -48,7 +48,7 @@ }, "required": [ "description", - "group", + "feature_category", "introduced_by_issue", "introduced_by_mr", "milestone", diff --git a/config/feature_flags/development/approval_rules_pagination.yml b/config/feature_flags/development/cache_project_integrations.yml index 78d4ad37ced..3bb652d4b51 100644 --- a/config/feature_flags/development/approval_rules_pagination.yml +++ b/config/feature_flags/development/cache_project_integrations.yml @@ -1,8 +1,8 @@ --- -name: approval_rules_pagination -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91702 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366823 -milestone: '15.2' +name: cache_project_integrations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104062 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/384004 +milestone: '15.7' type: development -group: group::source code -default_enabled: true +group: group::pipeline execution +default_enabled: false diff --git a/config/feature_flags/ops/purge_stale_security_findings.yml b/config/feature_flags/ops/purge_stale_security_findings.yml deleted file mode 100644 index b540c8a1d60..00000000000 --- a/config/feature_flags/ops/purge_stale_security_findings.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: purge_stale_security_findings -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81423 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356464 -milestone: '14.9' -type: ops -group: group::threat insights -default_enabled: true diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index c49fcfd5495..4d8d09313b6 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -526,6 +526,9 @@ Settings.cron_jobs['remove_unaccepted_member_invites_worker']['job_class'] = 'Re Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *' Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker' +Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['cron'] ||= '30 3 * * */7' +Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['job_class'] = 'Gitlab::Export::PruneProjectExportJobsWorker' Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *' Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker' diff --git a/danger/plugins/stable_branch.rb b/danger/plugins/stable_branch.rb new file mode 100644 index 00000000000..81dae35ff24 --- /dev/null +++ b/danger/plugins/stable_branch.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative '../../tooling/danger/stable_branch' + +module Danger + class StableBranch < ::Danger::Plugin + include Tooling::Danger::StableBranch + end +end diff --git a/danger/stable_branch_patch/Dangerfile b/danger/stable_branch_patch/Dangerfile new file mode 100644 index 00000000000..b3326e82020 --- /dev/null +++ b/danger/stable_branch_patch/Dangerfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +stable_branch.check! diff --git a/db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb b/db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb new file mode 100644 index 00000000000..1cf4a33e09f --- /dev/null +++ b/db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class ScheduleFixingSecurityScanStatuses < Gitlab::Database::Migration[2.0] + MIGRATION = 'FixSecurityScanStatuses' + TABLE_NAME = :security_scans + BATCH_COLUMN = :id + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + MAX_BATCH_SIZE = 50_000 + SUB_BATCH_SIZE = 100 + + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class SecurityScan < MigrationRecord + def self.start_migration_from + sort_order = Arel::Nodes::SqlLiteral.new("date(timezone('UTC'::text, created_at)) ASC, id ASC") + + where("date(timezone('UTC'::text, created_at)) > ?", 90.days.ago).order(sort_order).first&.id + end + end + + def up + # Only the SaaS application is affected + return unless Gitlab.dev_or_test_env? || Gitlab.com? + + batch_min_value = SecurityScan.start_migration_from + + return unless batch_min_value # It is possible that some users don't have corrupted records + + queue_batched_background_migration( + MIGRATION, + TABLE_NAME, + BATCH_COLUMN, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + max_batch_size: MAX_BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE, + batch_min_value: batch_min_value + ) + end + + def down + delete_batched_background_migration( + MIGRATION, + TABLE_NAME, + BATCH_COLUMN, + [] + ) + end +end diff --git a/db/post_migrate/20221129124240_remove_flowdock_integration_records.rb b/db/post_migrate/20221129124240_remove_flowdock_integration_records.rb new file mode 100644 index 00000000000..6390ed0d53b --- /dev/null +++ b/db/post_migrate/20221129124240_remove_flowdock_integration_records.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class RemoveFlowdockIntegrationRecords < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class Integration < MigrationRecord + include EachBatch + + self.table_name = 'integrations' + end + + def up + Integration.each_batch(of: 1000, column: :id) do |relation| + relation.delete_by(type_new: 'Integrations::Flowdock') + end + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20221128120634 b/db/schema_migrations/20221128120634 new file mode 100644 index 00000000000..4a2fa52d675 --- /dev/null +++ b/db/schema_migrations/20221128120634 @@ -0,0 +1 @@ +011a7add2949c39e642da2f9d7908f6e2a118c91f2e334e0eee623711576c3cb
\ No newline at end of file diff --git a/db/schema_migrations/20221129124240 b/db/schema_migrations/20221129124240 new file mode 100644 index 00000000000..9b0199dc748 --- /dev/null +++ b/db/schema_migrations/20221129124240 @@ -0,0 +1 @@ +ae20537326115d37db8beb3432ffd3ace447b39a75906535d319da4db1fcb1b2
\ No newline at end of file diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index 4d52bf36f20..bc91f2752c6 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -84,6 +84,7 @@ Supported attributes: > - Moved to GitLab Premium in 13.9. > - Pagination support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31011) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `approval_rules_pagination`. Enabled by default. > - `applies_to_all_protected_branches` property was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335316) in GitLab 15.3. +> - Pagination support [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366823) in GitLab 15.7. Feature flag `approval_rules_pagination` removed. You can request information about a project's approval rules using the following endpoint: @@ -704,6 +705,7 @@ Supported attributes: > - Moved to GitLab Premium in 13.9. > - Pagination support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31011) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `approval_rules_pagination`. Enabled by default. +> - Pagination support [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366823) in GitLab 15.7. Feature flag `approval_rules_pagination` removed. You can request information about a merge request's approval rules using the following endpoint: diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 93ff10a4132..e2340e39903 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -221,6 +221,9 @@ See the [test engineering process](https://about.gitlab.com/handbook/engineering 1. You have confirmed that if this MR contains changes to processing or storing of credentials or tokens, authorization, and authentication methods, or other items described in [the security review guidelines](https://about.gitlab.com/handbook/security/#when-to-request-a-security-review), you have added the `~security` label and you have `@`-mentioned `@gitlab-com/gl-security/appsec`. 1. You have reviewed the documentation regarding [internal application security reviews](https://about.gitlab.com/handbook/security/#internal-application-security-reviews) for **when** and **how** to request a security review and requested a security review if this is warranted for this change. +1. If there are security scan results that are blocking the MR (due to the [scan result policies](https://gitlab.com/gitlab-com/gl-security/security-policies)): + - For true positive findings, they should be corrected before the merge request is merged. This will remove the AppSec approval required by the scan result policy. + - For false positive findings, something that should be discussed for risk acceptance, or anything questionable, please ping `@gitlab-com/gl-security/appsec`. ##### Deployment diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb index 5dea41bfdb7..35fdcfe3ab0 100644 --- a/lib/api/merge_request_approvals.rb +++ b/lib/api/merge_request_approvals.rb @@ -88,6 +88,7 @@ module API present_approval(merge_request) end + desc 'Remove all merge request approvals' do detail 'Clear all approvals of merge request. This feature was added in GitLab 15.4' failure [ diff --git a/lib/gitlab/audit/type/shared.rb b/lib/gitlab/audit/type/shared.rb index 999b7de13e2..1e3e26d3735 100644 --- a/lib/gitlab/audit/type/shared.rb +++ b/lib/gitlab/audit/type/shared.rb @@ -14,7 +14,7 @@ module Gitlab description introduced_by_issue introduced_by_mr - group + feature_category milestone saved_to_database streamed diff --git a/lib/gitlab/background_migration/fix_security_scan_statuses.rb b/lib/gitlab/background_migration/fix_security_scan_statuses.rb new file mode 100644 index 00000000000..b60e739f870 --- /dev/null +++ b/lib/gitlab/background_migration/fix_security_scan_statuses.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Fixes the `status` attribute of `security_scans` records + class FixSecurityScanStatuses < BatchedMigrationJob + def perform + # no-op. The logic is defined in EE module. + end + end + end +end + +::Gitlab::BackgroundMigration::FixSecurityScanStatuses.prepend_mod diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 2d9c8d1108e..cc69ed55744 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -763,6 +763,19 @@ excluded_attributes: - :import_type - :import_source - :integrations + - :push_hooks_integrations + - :tag_push_hooks_integrations + - :issue_hooks_integrations + - :confidential_issue_hooks_integrations + - :merge_request_hooks_integrations + - :note_hooks_integrations + - :confidential_note_hooks_integrations + - :job_hooks_integrations + - :archive_trace_hooks_integrations + - :pipeline_hooks_integrations + - :wiki_page_hooks_integrations + - :deployment_hooks_integrations + - :alert_hooks_integrations - :mirror - :runners_token - :runners_token_encrypted @@ -1209,7 +1222,9 @@ ee: - :description iterations_cadence: - :title - + excluded_attributes: + project: + - :vulnerability_hooks_integrations preloads: issues: epic: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1b17569ef84..a879bb8b976 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13803,6 +13803,9 @@ msgstr "" msgid "Description:" msgstr "" +msgid "Descriptions" +msgstr "" + msgid "Descriptive label" msgstr "" @@ -36199,6 +36202,9 @@ msgstr "" msgid "Search GitLab" msgstr "" +msgid "Search Within" +msgstr "" + msgid "Search a group" msgstr "" @@ -42779,6 +42785,9 @@ msgstr "" msgid "Title:" msgstr "" +msgid "Titles" +msgstr "" + msgid "Titles and Descriptions" msgstr "" diff --git a/qa/qa/support/loglinking.rb b/qa/qa/support/loglinking.rb index 74d72ca3a3e..5a1aad3c7eb 100644 --- a/qa/qa/support/loglinking.rb +++ b/qa/qa/support/loglinking.rb @@ -22,8 +22,8 @@ module QA pre: 'https://nonprod-log.gitlab.net/' }.freeze KIBANA_INDICES = { - staging: 'pubsub-rails-inf-gstg', - production: 'pubsub-rails-inf-gprd', + staging: 'ed942d00-5186-11ea-ad8a-f3610a492295', + production: '7092c4e2-4eb5-46f2-8305-a7da2edad090', pre: 'pubsub-rails-inf-pre' }.freeze @@ -64,7 +64,7 @@ module QA end def get_kibana_url(base_url, index, correlation_id) - "#{base_url}app/discover#/?_a=%28index:#{index}%2Cquery%3A%28language%3Akuery%2C" \ + "#{base_url}app/discover#/?_a=%28index:%27#{index}%27%2Cquery%3A%28language%3Akuery%2C" \ "query%3A%27json.correlation_id%20%3A%20#{correlation_id}%27%29%29" \ "&_g=%28time%3A%28from%3A%27#{start_time}%27%2Cto%3A%27#{end_time}%27%29%29" end diff --git a/qa/spec/resource/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb index cf47574fcce..76cc8e0e303 100644 --- a/qa/spec/resource/api_fabricator_spec.rb +++ b/qa/spec/resource/api_fabricator_spec.rb @@ -157,7 +157,7 @@ RSpec.describe QA::Resource::ApiFabricator do Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`. Correlation Id: foobar Sentry Url: https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg&query=correlation_id%3A%22foobar%22 - Kibana Url: https://nonprod-log.gitlab.net/app/discover#/?_a=%28index:pubsub-rails-inf-gstg%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foobar%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29 + Kibana Url: https://nonprod-log.gitlab.net/app/discover#/?_a=%28index:%27ed942d00-5186-11ea-ad8a-f3610a492295%27%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foobar%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29 ERROR end end diff --git a/qa/spec/support/loglinking_spec.rb b/qa/spec/support/loglinking_spec.rb index 24b157ae9c3..3955d266ef6 100644 --- a/qa/spec/support/loglinking_spec.rb +++ b/qa/spec/support/loglinking_spec.rb @@ -37,7 +37,7 @@ RSpec.describe QA::Support::Loglinking do expect(QA::Support::Loglinking.failure_metadata('foo123')).to eql(<<~ERROR.chomp) Correlation Id: foo123 - Kibana Url: https://kibana.address/app/discover#/?_a=%28index:pubsub-rails-inf-foo%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foo123%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29 + Kibana Url: https://kibana.address/app/discover#/?_a=%28index:%27pubsub-rails-inf-foo%27%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foo123%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29 ERROR end end @@ -93,9 +93,9 @@ RSpec.describe QA::Support::Loglinking do describe '.get_kibana_index' do let(:index_hash) do { - :staging => 'pubsub-rails-inf-gstg', + :staging => 'ed942d00-5186-11ea-ad8a-f3610a492295', :staging_ref => nil, - :production => 'pubsub-rails-inf-gprd', + :production => '7092c4e2-4eb5-46f2-8305-a7da2edad090', :pre => 'pubsub-rails-inf-pre', :foo => nil, nil => nil diff --git a/spec/bin/audit_event_type_spec.rb b/spec/bin/audit_event_type_spec.rb index dbf22898525..e23d365f68f 100644 --- a/spec/bin/audit_event_type_spec.rb +++ b/spec/bin/audit_event_type_spec.rb @@ -9,7 +9,7 @@ RSpec.describe 'bin/audit-event-type' do using RSpec::Parameterized::TableSyntax describe AuditEventTypeCreator do - let(:argv) { %w[test_audit_event -d test -g govern::compliance -s -t -i https://url -m http://url] } + let(:argv) { %w[test_audit_event -d test -c compliance_management -s -t -i https://url -m http://url] } let(:options) { AuditEventTypeOptionParser.parse(argv) } let(:creator) { described_class.new(options) } let(:existing_audit_event_types) do @@ -71,8 +71,8 @@ RSpec.describe 'bin/audit-event-type' do :force | %w[foo --force] | true :description | %w[foo -d desc] | 'desc' :description | %w[foo --description desc] | 'desc' - :group | %w[foo -g govern::compliance] | 'govern::compliance' - :group | %w[foo --group govern::compliance] | 'govern::compliance' + :feature_category | %w[foo -c audit_events] | 'audit_events' + :feature_category | %w[foo --feature-category audit_events] | 'audit_events' :milestone | %w[foo -M 15.6] | '15.6' :milestone | %w[foo --milestone 15.6] | '15.6' :saved_to_database | %w[foo -s] | true @@ -141,27 +141,27 @@ RSpec.describe 'bin/audit-event-type' do end end - describe '.read_group' do - let(:group) { 'govern::compliance' } + describe '.read_feature_category' do + let(:feature_category) { 'compliance_management' } - it 'reads group from stdin' do - expect(Readline).to receive(:readline).and_return(group) + it 'reads feature_category from stdin' do + expect(Readline).to receive(:readline).and_return(feature_category) expect do - expect(described_class.read_group).to eq('govern::compliance') - end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/).to_stdout + expect(described_class.read_feature_category).to eq('compliance_management') + end.to output(/Specify the feature category of this audit event, like `compliance_management`:/).to_stdout end - context 'when group is empty' do - let(:group) { '' } + context 'when feature category is empty' do + let(:feature_category) { '' } it 'shows error message and retries' do - expect(Readline).to receive(:readline).and_return(group) + expect(Readline).to receive(:readline).and_return(feature_category) expect(Readline).to receive(:readline).and_raise('EOF') expect do - expect { described_class.read_group }.to raise_error(/EOF/) - end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/) - .to_stdout.and output(/group is a required field/).to_stderr + expect { described_class.read_feature_category }.to raise_error(/EOF/) + end.to output(/Specify the feature category of this audit event, like `compliance_management`:/) + .to_stdout.and output(/feature_category is a required field/).to_stderr end end end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 6146e649d70..9bede49268e 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -237,7 +237,8 @@ RSpec.describe 'Database schema' do "Packages::Composer::Metadatum" => %w[composer_json], "RawUsageData" => %w[payload], # Usage data payload changes often, we cannot use one schema "Releases::Evidence" => %w[summary], - "Vulnerabilities::Finding::Evidence" => %w[data] # Validation work in progress + "Vulnerabilities::Finding::Evidence" => %w[data], # Validation work in progress + "EE::Gitlab::BackgroundMigration::FixSecurityScanStatuses::SecurityScan" => %w[info] # This is a migration model }.freeze # We are skipping GEO models for now as it adds up complexity diff --git a/spec/factories/projects/import_export/relation_export_upload.rb b/spec/factories/projects/import_export/relation_export_upload.rb index eaa57d6ee59..4bd6a586720 100644 --- a/spec/factories/projects/import_export/relation_export_upload.rb +++ b/spec/factories/projects/import_export/relation_export_upload.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true FactoryBot.define do - factory :project_relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do + factory :relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do relation_export factory: :project_relation_export export_file { fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz") } end diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js index 3195d5ff0a1..96b8daa22d8 100644 --- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js +++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js @@ -7,10 +7,17 @@ import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue'; import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; +import setWindowLocation from 'helpers/set_window_location_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { + setSortPreferenceMutationResponse, + setSortPreferenceMutationResponseWithErrors, +} from 'jest/issues/list/mock_data'; import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue'; -import { i18n } from '~/issues/list/constants'; +import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants'; +import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql'; +import { getSortKey, getSortOptions } from '~/issues/list/utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import { IssuableStates } from '~/vue_shared/issuable/list/constants'; @@ -31,6 +38,7 @@ describe('IssuesDashboardApp component', () => { hasIssuableHealthStatusFeature: true, hasIssueWeightsFeature: true, hasScopedLabelsFeature: true, + initialSort: CREATED_DESC, isPublicVisibilityRestricted: false, isSignedIn: true, rssPath: 'rss/path', @@ -54,11 +62,19 @@ describe('IssuesDashboardApp component', () => { wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText }); const mountComponent = ({ + provide = {}, issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse), + sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse), } = {}) => { wrapper = mountExtended(IssuesDashboardApp, { - apolloProvider: createMockApollo([[getIssuesQuery, issuesQueryHandler]]), - provide: defaultProvide, + apolloProvider: createMockApollo([ + [getIssuesQuery, issuesQueryHandler], + [setSortPreferenceMutation, sortPreferenceMutationResponse], + ]), + provide: { + ...defaultProvide, + ...provide, + }, }); }; @@ -71,11 +87,23 @@ describe('IssuesDashboardApp component', () => { hasNextPage: true, hasPreviousPage: false, hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature, + initialSortBy: CREATED_DESC, + issuables: issuesQueryResponse.data.issues.nodes, + issuablesLoading: false, namespace: 'dashboard', recentSearchesStorageKey: 'issues', searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder, showPaginationControls: true, + sortOptions: getSortOptions({ + hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature, + hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature, + hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature, + }), tabs: IssuesDashboardApp.IssuableListTabs, + urlParams: { + sort: urlSortParams[CREATED_DESC], + state: IssuableStates.Opened, + }, useKeysetPagination: true, }); }); @@ -118,6 +146,51 @@ describe('IssuesDashboardApp component', () => { }); }); + describe('initial url params', () => { + describe('sort', () => { + describe('when initial sort value uses old enum values', () => { + const oldEnumSortValues = Object.values(urlSortParams); + + it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => { + mountComponent({ provide: { initialSort: sort } }); + + expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort)); + }); + }); + + describe('when initial sort value uses new GraphQL enum values', () => { + const graphQLEnumSortValues = Object.keys(urlSortParams); + + it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => { + mountComponent({ provide: { initialSort: sort.toLowerCase() } }); + + expect(findIssuableList().props('initialSortBy')).toBe(sort); + }); + }); + + describe('when initial sort value is invalid', () => { + it.each(['', 'asdf', null, undefined])( + 'initial sort is set to value CREATED_DESC', + (sort) => { + mountComponent({ provide: { initialSort: sort } }); + + expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC); + }, + ); + }); + }); + + describe('state', () => { + it('is set from the url params', () => { + const initialState = IssuableStates.All; + setWindowLocation(`?state=${initialState}`); + mountComponent(); + + expect(findIssuableList().props('currentTab')).toBe(initialState); + }); + }); + }); + describe('when there is an error fetching issues', () => { beforeEach(() => { mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) }); @@ -148,6 +221,12 @@ describe('IssuesDashboardApp component', () => { it('updates ui to the new tab', () => { expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed); }); + + it('updates url to the new tab', () => { + expect(findIssuableList().props('urlParams')).toMatchObject({ + state: IssuableStates.Closed, + }); + }); }); describe.each(['next-page', 'previous-page'])( @@ -164,5 +243,63 @@ describe('IssuesDashboardApp component', () => { }); }, ); + + describe('when "sort" event is emitted by IssuableList', () => { + it.each(Object.keys(urlSortParams))( + 'updates to the new sort when payload is `%s`', + async (sortKey) => { + // Ensure initial sort key is different so we can trigger an update when emitting a sort key + if (sortKey === CREATED_DESC) { + mountComponent({ provide: { initialSort: UPDATED_DESC } }); + } else { + mountComponent(); + } + + findIssuableList().vm.$emit('sort', sortKey); + await nextTick(); + + expect(findIssuableList().props('urlParams')).toMatchObject({ + sort: urlSortParams[sortKey], + }); + }, + ); + + describe('when user is signed in', () => { + it('calls mutation to save sort preference', () => { + const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse); + mountComponent({ sortPreferenceMutationResponse: mutationMock }); + + findIssuableList().vm.$emit('sort', UPDATED_DESC); + + expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } }); + }); + + it('captures error when mutation response has errors', async () => { + const mutationMock = jest + .fn() + .mockResolvedValue(setSortPreferenceMutationResponseWithErrors); + mountComponent({ sortPreferenceMutationResponse: mutationMock }); + + findIssuableList().vm.$emit('sort', UPDATED_DESC); + await waitForPromises(); + + expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!')); + }); + }); + + describe('when user is signed out', () => { + it('does not call mutation to save sort preference', () => { + const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse); + mountComponent({ + provide: { isSignedIn: false }, + sortPreferenceMutationResponse: mutationMock, + }); + + findIssuableList().vm.$emit('sort', CREATED_DESC); + + expect(mutationMock).not.toHaveBeenCalled(); + }); + }); + }); }); }); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index b820b7d02bc..d404225333a 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -61,6 +61,7 @@ import { TOKEN_TYPE_MY_REACTION, TOKEN_TYPE_ORGANIZATION, TOKEN_TYPE_RELEASE, + TOKEN_TYPE_SEARCH_WITHIN, TOKEN_TYPE_TYPE, } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -597,6 +598,7 @@ describe('CE IssuesListApp component', () => { { type: TOKEN_TYPE_MY_REACTION }, { type: TOKEN_TYPE_ORGANIZATION }, { type: TOKEN_TYPE_RELEASE }, + { type: TOKEN_TYPE_SEARCH_WITHIN }, { type: TOKEN_TYPE_TYPE }, ]); }); diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 83b863eb7e3..ed363268cdf 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' RSpec.describe IssuesHelper do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let(:ext_project) { create(:project, :with_redmine_integration) } + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:issue) { create(:issue, project: project) } describe '#work_item_type_icon' do it 'returns icon of all standard base types' do @@ -392,6 +391,7 @@ RSpec.describe IssuesHelper do expected = { calendar_path: '#', empty_state_svg_path: '#', + initial_sort: current_user&.user_preference&.issues_sort, is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '', is_signed_in: current_user.present?.to_s, rss_path: '#' diff --git a/spec/lib/gitlab/audit/type/definition_spec.rb b/spec/lib/gitlab/audit/type/definition_spec.rb index bd3b90f12f0..3d11edf84f7 100644 --- a/spec/lib/gitlab/audit/type/definition_spec.rb +++ b/spec/lib/gitlab/audit/type/definition_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do description: 'Group deploy token is deleted', introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/1', introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1', - group: 'govern::compliance', + feature_category: 'continuous_delivery', milestone: '15.4', saved_to_database: true, streamed: true } @@ -42,7 +42,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do :description | nil | %r{property '/description' is not of type: string} :introduced_by_issue | nil | %r{property '/introduced_by_issue' is not of type: string} :introduced_by_mr | nil | %r{property '/introduced_by_mr' is not of type: string} - :group | nil | %r{property '/group' is not of type: string} + :feature_category | nil | %r{property '/feature_category' is not of type: string} :milestone | nil | %r{property '/milestone' is not of type: string} end # rubocop:enable Layout/LineLength @@ -109,7 +109,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do expect(audit_event_type_definition.name).to eq "group_deploy_token_destroyed" expect(audit_event_type_definition.description).to eq "Group deploy token is deleted" - expect(audit_event_type_definition.group).to eq "govern::compliance" + expect(audit_event_type_definition.feature_category).to eq "continuous_delivery" expect(audit_event_type_definition.milestone).to eq "15.4" expect(audit_event_type_definition.saved_to_database).to be true expect(audit_event_type_definition.streamed).to be true diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index b9867f794dc..b34399d20f1 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -409,6 +409,20 @@ project: - boards - last_event - integrations +- push_hooks_integrations +- tag_push_hooks_integrations +- issue_hooks_integrations +- confidential_issue_hooks_integrations +- merge_request_hooks_integrations +- note_hooks_integrations +- confidential_note_hooks_integrations +- job_hooks_integrations +- archive_trace_hooks_integrations +- pipeline_hooks_integrations +- wiki_page_hooks_integrations +- deployment_hooks_integrations +- alert_hooks_integrations +- vulnerability_hooks_integrations - campfire_integration - confluence_integration - datadog_integration diff --git a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb index a781139acab..d70e89c6856 100644 --- a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb +++ b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb @@ -8,17 +8,17 @@ RSpec.describe Gitlab::ImportExport::Project::ExportedRelationsMerger do let(:shared) { Gitlab::ImportExport::Shared.new(export_job.project) } before do - create(:project_relation_export_upload, + create(:relation_export_upload, relation_export: create(:project_relation_export, relation: 'project', project_export_job: export_job), export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/project.tar.gz") ) - create(:project_relation_export_upload, + create(:relation_export_upload, relation_export: create(:project_relation_export, relation: 'labels', project_export_job: export_job), export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz") ) - create(:project_relation_export_upload, + create(:relation_export_upload, relation_export: create(:project_relation_export, relation: 'uploads', project_export_job: export_job), export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/uploads.tar.gz") ) diff --git a/spec/migrations/remove_flowdock_integration_records_spec.rb b/spec/migrations/remove_flowdock_integration_records_spec.rb new file mode 100644 index 00000000000..3f57515d18b --- /dev/null +++ b/spec/migrations/remove_flowdock_integration_records_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db/post_migrate/20221129124240_remove_flowdock_integration_records.rb') + +RSpec.describe RemoveFlowdockIntegrationRecords, feature_category: :integrations do + let(:integrations) { table(:integrations) } + + before do + integrations.create!(type_new: 'Integrations::Flowdock') + integrations.create!(type_new: 'SomeOtherType') + end + + it 'removes integrations records of type_new Integrations::Flowdock' do + expect(integrations.count).to eq(2) + + migrate! + + expect(integrations.count).to eq(1) + expect(integrations.first.type_new).to eq('SomeOtherType') + expect(integrations.where(type_new: 'Integrations::Flowdock')).to be_empty + end +end diff --git a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb new file mode 100644 index 00000000000..e958593dc19 --- /dev/null +++ b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleFixingSecurityScanStatuses, feature_category: :vulnerability_management do + let_it_be(:namespaces) { table(:namespaces) } + let_it_be(:projects) { table(:projects) } + let_it_be(:pipelines) { table(:ci_pipelines) } + let_it_be(:builds) { table(:ci_builds) } + let_it_be(:security_scans) { table(:security_scans) } + + let_it_be(:namespace) { namespaces.create!(name: "foo", path: "bar") } + let_it_be(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } + let_it_be(:pipeline) do + pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success', partition_id: 1) + end + + let_it_be(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 1) } + + let!(:security_scan_1) { security_scans.create!(build_id: ci_build.id, scan_type: 1, created_at: 91.days.ago) } + let!(:security_scan_2) { security_scans.create!(build_id: ci_build.id, scan_type: 2) } + + let(:com?) { false } + let(:dev_or_test_env?) { false } + let(:migration) { described_class::MIGRATION } + + before do + allow(::Gitlab).to receive(:com?).and_return(com?) + allow(::Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test_env?) + + migrate! + end + + describe '#up' do + shared_examples_for 'scheduler for fixing the security scans status' do + it 'schedules background job' do + expect(migration).to have_scheduled_batched_migration( + table_name: :security_scans, + column_name: :id, + interval: 2.minutes, + batch_size: 10_000, + max_batch_size: 50_000, + sub_batch_size: 100, + batch_min_value: security_scan_2.id + ) + end + end + + context 'when the migration does not run on GitLab.com or development environment' do + it 'does not schedule the migration' do + expect('FixSecurityScanStatuses').not_to have_scheduled_batched_migration + end + end + + context 'when the migration runs on GitLab.com' do + let(:com?) { true } + + it_behaves_like 'scheduler for fixing the security scans status' + end + + context 'when the migration runs on dev environment' do + let(:dev_or_test_env?) { true } + + it_behaves_like 'scheduler for fixing the security scans status' + end + end + + describe '#down' do + it 'deletes all batched migration records' do + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index aa31e32036f..a4ccae459cf 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -587,7 +587,7 @@ RSpec.describe Integrations::Jira do close_issue end - it_behaves_like 'Snowplow event tracking' do + it_behaves_like 'Snowplow event tracking with RedisHLL context' do subject { close_issue } let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } @@ -943,7 +943,7 @@ RSpec.describe Integrations::Jira do subject end - it_behaves_like 'Snowplow event tracking' do + it_behaves_like 'Snowplow event tracking with RedisHLL context' do let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } let(:category) { 'Integrations::Jira' } let(:action) { 'perform_integrations_action' } diff --git a/spec/models/project_export_job_spec.rb b/spec/models/project_export_job_spec.rb index 653d4d2df27..01b0aaff0ff 100644 --- a/spec/models/project_export_job_spec.rb +++ b/spec/models/project_export_job_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectExportJob, type: :model do +RSpec.describe ProjectExportJob, feature_category: :importers, type: :model do describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:relation_exports) } @@ -13,4 +13,54 @@ RSpec.describe ProjectExportJob, type: :model do it { is_expected.to validate_presence_of(:jid) } it { is_expected.to validate_presence_of(:status) } end + + context 'when pruning expired jobs' do + let_it_be(:old_job_1) { create(:project_export_job, updated_at: 37.months.ago) } + let_it_be(:old_job_2) { create(:project_export_job, updated_at: 12.months.ago) } + let_it_be(:old_job_3) { create(:project_export_job, updated_at: 8.days.ago) } + let_it_be(:fresh_job_1) { create(:project_export_job, updated_at: 1.day.ago) } + let_it_be(:fresh_job_2) { create(:project_export_job, updated_at: 2.days.ago) } + let_it_be(:fresh_job_3) { create(:project_export_job, updated_at: 6.days.ago) } + + let_it_be(:old_relation_export_1) { create(:project_relation_export, project_export_job_id: old_job_1.id) } + let_it_be(:old_relation_export_2) { create(:project_relation_export, project_export_job_id: old_job_2.id) } + let_it_be(:old_relation_export_3) { create(:project_relation_export, project_export_job_id: old_job_3.id) } + let_it_be(:fresh_relation_export_1) { create(:project_relation_export, project_export_job_id: fresh_job_1.id) } + + let_it_be(:old_upload_1) { create(:relation_export_upload, project_relation_export_id: old_relation_export_1.id) } + let_it_be(:old_upload_2) { create(:relation_export_upload, project_relation_export_id: old_relation_export_2.id) } + let_it_be(:old_upload_3) { create(:relation_export_upload, project_relation_export_id: old_relation_export_3.id) } + let_it_be(:fresh_upload_1) do + create( + :relation_export_upload, + project_relation_export_id: fresh_relation_export_1.id + ) + end + + it 'prunes jobs and associations older than 7 days' do + expect { described_class.prune_expired_jobs }.to change { described_class.count }.by(-3) + + expect(described_class.find_by(id: old_job_1.id)).to be_nil + expect(described_class.find_by(id: old_job_2.id)).to be_nil + expect(described_class.find_by(id: old_job_3.id)).to be_nil + + expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_1.id)).to be_nil + expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_2.id)).to be_nil + expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_3.id)).to be_nil + + expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_1.id)).to be_nil + expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_2.id)).to be_nil + expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_3.id)).to be_nil + end + + it 'does not delete associated records for jobs younger than 7 days' do + described_class.prune_expired_jobs + + expect(fresh_job_1.reload).to be_present + expect(fresh_job_2.reload).to be_present + expect(fresh_job_3.reload).to be_present + expect(fresh_relation_export_1.reload).to be_present + expect(fresh_upload_1.reload).to be_present + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e800a468f8c..9a4179b9157 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -21,7 +21,6 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to belong_to(:creator).class_name('User') } it { is_expected.to belong_to(:pool_repository) } it { is_expected.to have_many(:users) } - it { is_expected.to have_many(:integrations) } it { is_expected.to have_many(:events) } it { is_expected.to have_many(:merge_requests) } it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') } @@ -150,6 +149,20 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout').with_foreign_key(:project_id) } it { is_expected.to have_many(:pipeline_metadata).class_name('Ci::PipelineMetadata') } it { is_expected.to have_many(:incident_management_timeline_event_tags).class_name('IncidentManagement::TimelineEventTag') } + it { is_expected.to have_many(:integrations) } + it { is_expected.to have_many(:push_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:tag_push_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:issue_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:confidential_issue_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:merge_request_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:note_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:confidential_note_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:job_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:archive_trace_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:pipeline_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:wiki_page_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:deployment_hooks_integrations).class_name('Integration') } + it { is_expected.to have_many(:alert_hooks_integrations).class_name('Integration') } # GitLab Pages it { is_expected.to have_many(:pages_domains) } @@ -5758,6 +5771,32 @@ RSpec.describe Project, factory_default: :keep do integration.project.execute_integrations(anything, :merge_request_hooks) end + + it 'does not trigger extra queries when called multiple times' do + integration.project.execute_integrations({}, :push_hooks) + + recorder = ActiveRecord::QueryRecorder.new do + integration.project.execute_integrations({}, :push_hooks) + end + + expect(recorder.count).to be_zero + end + + context 'with cache_project_integrations disabled' do + before do + stub_feature_flags(cache_project_integrations: false) + end + + it 'triggers extra queries when called multiple times' do + integration.project.execute_integrations({}, :push_hooks) + + recorder = ActiveRecord::QueryRecorder.new do + integration.project.execute_integrations({}, :push_hooks) + end + + expect(recorder.count).not_to be_zero + end + end end describe '#has_active_hooks?' do diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb index 7e1af132b1d..741a0db3009 100644 --- a/spec/policies/merge_request_policy_spec.rb +++ b/spec/policies/merge_request_policy_spec.rb @@ -10,6 +10,7 @@ RSpec.describe MergeRequestPolicy do let_it_be(:reporter) { create(:user) } let_it_be(:developer) { create(:user) } let_it_be(:non_team_member) { create(:user) } + let_it_be(:bot) { create(:user, :project_bot) } def permissions(user, merge_request) described_class.new(user, merge_request) @@ -72,6 +73,7 @@ RSpec.describe MergeRequestPolicy do project.add_guest(guest) project.add_guest(author) project.add_developer(developer) + project.add_developer(bot) end context 'when merge request is public' do @@ -95,6 +97,18 @@ RSpec.describe MergeRequestPolicy do it do is_expected.to be_allowed(:approve_merge_request) end + + it do + is_expected.to be_disallowed(:reset_merge_request_approvals) + end + end + + context 'and the user is a bot' do + let(:user) { bot } + + it do + is_expected.to be_allowed(:reset_merge_request_approvals) + end end end end @@ -123,6 +137,14 @@ RSpec.describe MergeRequestPolicy do it_behaves_like 'a denied user' end + + describe 'a bot' do + let(:subject) { permissions(bot, merge_request) } + + it do + is_expected.to be_disallowed(:reset_merge_request_approvals) + end + end end context 'when merge requests are private' do @@ -144,6 +166,14 @@ RSpec.describe MergeRequestPolicy do it_behaves_like 'a user with full access' end + + describe 'a bot' do + let(:subject) { permissions(bot, merge_request) } + + it do + is_expected.to be_allowed(:reset_merge_request_approvals) + end + end end context 'when merge request is unlocked' do @@ -214,6 +244,7 @@ RSpec.describe MergeRequestPolicy do group.add_guest(author) group.add_reporter(reporter) group.add_developer(developer) + group.add_developer(bot) end context 'when project is public' do @@ -222,9 +253,25 @@ RSpec.describe MergeRequestPolicy do describe 'the merge request author' do subject { permissions(author, merge_request) } - specify do + it do is_expected.to be_allowed(:approve_merge_request) end + + it do + is_expected.to be_disallowed(:reset_merge_request_approvals) + end + end + + describe 'a bot' do + subject { permissions(bot, merge_request) } + + it do + is_expected.to be_allowed(:approve_merge_request) + end + + it do + is_expected.to be_allowed(:reset_merge_request_approvals) + end end context 'and merge requests are private' do @@ -250,6 +297,14 @@ RSpec.describe MergeRequestPolicy do it_behaves_like 'a user with full access' end + + describe 'a bot' do + let(:subject) { permissions(bot, merge_request) } + + it do + is_expected.to be_allowed(:reset_merge_request_approvals) + end + end end end @@ -273,6 +328,14 @@ RSpec.describe MergeRequestPolicy do it_behaves_like 'a user with full access' end + + describe 'a bot' do + let(:subject) { permissions(bot, merge_request) } + + it do + is_expected.to be_allowed(:reset_merge_request_approvals) + end + end end end @@ -297,11 +360,28 @@ RSpec.describe MergeRequestPolicy do group_access: Gitlab::Access::DEVELOPER) group.add_guest(non_team_member) + group.add_guest(bot) end - specify do + it do is_expected.to be_allowed(:approve_merge_request) end + + it do + is_expected.to be_disallowed(:reset_merge_request_approvals) + end + + context 'and the user is a bot' do + let(:user) { bot } + + it do + is_expected.to be_allowed(:approve_merge_request) + end + + it do + is_expected.to be_allowed(:reset_merge_request_approvals) + end + end end end @@ -313,9 +393,25 @@ RSpec.describe MergeRequestPolicy do subject { permissions(non_team_member, merge_request) } - specify do + it do is_expected.not_to be_allowed(:approve_merge_request) end + + it do + is_expected.not_to be_allowed(:reset_merge_request_approvals) + end + + context 'and the user is a bot' do + subject { permissions(bot, merge_request) } + + it do + is_expected.not_to be_allowed(:approve_merge_request) + end + + it do + is_expected.not_to be_allowed(:reset_merge_request_approvals) + end + end end context 'when merge requests are disabled' do diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb index b18f3017e03..ba0039f46f8 100644 --- a/spec/requests/api/merge_request_approvals_spec.rb +++ b/spec/requests/api/merge_request_approvals_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' RSpec.describe API::MergeRequestApprovals do let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:bot) { create(:user, :project_bot) } let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } let_it_be(:approver) { create :user } let_it_be(:group) { create :group } @@ -87,4 +89,83 @@ RSpec.describe API::MergeRequestApprovals do end end end + + describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do + before do + merge_request.approvals.create!(user: user2) + create(:project_member, :maintainer, user: bot, source: project) + end + + context 'for a bot user' do + it 'clears approvals of the merge_request' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:accepted) + expect(merge_request.approvals).to be_empty + end + + context 'when bot user approved the merge request' do + before do + merge_request.approvals.create!(user: bot) + end + + it 'clears approvals of the merge_request' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:accepted) + expect(merge_request.approvals).to be_empty + end + end + end + + context 'for users with non-bot roles' do + let(:human_user) { create(:user) } + + [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method| + it 'returns 401' do + project.send(role_method, human_user) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user) + + merge_request.reload + expect(response).to have_gitlab_http_status(:unauthorized) + expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id) + end + end + end + + context 'for bot-users from external namespaces' do + let_it_be(:external_bot) { create(:user, :project_bot) } + + context 'for external group bot-user' do + before do + create(:group_member, :maintainer, user: external_bot, source: create(:group)) + end + + it 'returns 401' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:unauthorized) + expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id) + end + end + + context 'for external project bot-user' do + before do + create(:project_member, :maintainer, user: external_bot, source: create(:project)) + end + + it 'returns 401' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:unauthorized) + expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id) + end + end + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0affc522d4e..aa0abe3fe64 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,7 +9,6 @@ RSpec.describe API::MergeRequests do let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } let_it_be(:admin) { create(:user, :admin) } - let_it_be(:bot) { create(:user, :project_bot) } let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } @@ -3612,85 +3611,6 @@ RSpec.describe API::MergeRequests do end end - describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do - before do - merge_request.approvals.create!(user: user2) - create(:project_member, :maintainer, user: bot, source: project) - end - - context 'when reset_approvals can be performed' do - it 'clears approvals of the merge_request' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) - - merge_request.reload - expect(response).to have_gitlab_http_status(:accepted) - expect(merge_request.approvals).to be_empty - end - - context 'for users with non-bot roles' do - let(:human_user) { create(:user) } - - [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method| - it 'returns 401' do - project.send(role_method, human_user) - - put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user) - - merge_request.reload - expect(response).to have_gitlab_http_status(:unauthorized) - expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id]) - end - end - end - - context 'for bot-users from external namespaces' do - let_it_be(:external_bot) { create(:user, :project_bot) } - - context 'external group bot-user' do - before do - create(:group_member, :maintainer, user: external_bot, source: create(:group)) - end - - it 'returns 401' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) - - merge_request.reload - expect(response).to have_gitlab_http_status(:unauthorized) - expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id]) - end - end - - context 'external project bot-user' do - before do - create(:project_member, :maintainer, user: external_bot, source: create(:project)) - end - - it 'returns 401' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) - - merge_request.reload - expect(response).to have_gitlab_http_status(:unauthorized) - expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id]) - end - end - end - - context 'for a bot user who approved the merge request' do - before do - merge_request.approvals.create!(user: bot) - end - - it "returns 200" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) - - merge_request.reload - expect(response).to have_gitlab_http_status(:accepted) - expect(merge_request.approvals).to be_empty - end - end - end - end - describe 'Time tracking' do let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) } diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb index cde906e64c2..2e528f7996c 100644 --- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb @@ -34,7 +34,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:| execute end - it_behaves_like 'Snowplow event tracking' do + it_behaves_like 'Snowplow event tracking with RedisHLL context' do let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } let(:category) { described_class.to_s } let(:action) { 'perform_integrations_action' } diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb new file mode 100644 index 00000000000..08fd25b30e0 --- /dev/null +++ b/spec/tooling/danger/stable_branch_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require 'gitlab-dangerfiles' +require 'gitlab/dangerfiles/spec_helper' +require 'rspec-parameterized' +require 'httparty' + +require_relative '../../../tooling/danger/stable_branch' + +RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do + using RSpec::Parameterized::TableSyntax + + include_context 'with dangerfile' + let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } + + let(:stable_branch) { fake_danger.new(helper: fake_helper) } + + describe '#check!' do + subject { stable_branch.check! } + + shared_examples 'without a failure' do + it 'does not add a failure' do + expect(stable_branch).not_to receive(:fail) + + subject + end + end + + shared_examples 'with a failure' do |failure_message| + it 'fails' do + expect(stable_branch).to receive(:fail).with(failure_message) + + subject + end + end + + context 'when not applicable' do + where(:stable_branch?, :security_mr?) do + true | true + false | true + false | false + end + + with_them do + before do + allow(fake_helper).to receive(:mr_target_branch).and_return(stable_branch? ? '15-1-stable-ee' : 'main') + allow(fake_helper).to receive(:security_mr?).and_return(security_mr?) + end + + it_behaves_like "without a failure" + end + end + + context 'when applicable' do + let(:target_branch) { '15-1-stable-ee' } + let(:feature_label_present) { false } + let(:bug_label_present) { true } + let(:response_success) { true } + let(:parsed_response) do + [ + { 'version' => '15.1.1' }, + { 'version' => '15.1.0' }, + { 'version' => '15.0.2' }, + { 'version' => '15.0.1' }, + { 'version' => '15.0.0' }, + { 'version' => '14.10.3' }, + { 'version' => '14.10.2' }, + { 'version' => '14.9.3' } + ] + end + + let(:version_response) do + instance_double( + HTTParty::Response, + success?: response_success, + parsed_response: parsed_response + ) + end + + before do + allow(fake_helper).to receive(:mr_target_branch).and_return(target_branch) + allow(fake_helper).to receive(:security_mr?).and_return(false) + allow(fake_helper).to receive(:mr_has_labels?).with('type::feature').and_return(feature_label_present) + allow(fake_helper).to receive(:mr_has_labels?).with('type::bug').and_return(bug_label_present) + allow(HTTParty).to receive(:get).with(/page=1/).and_return(version_response) + end + + # the stubbed behavior above is the success path + it_behaves_like "without a failure" + + context 'with a feature label' do + let(:feature_label_present) { true } + + it_behaves_like 'with a failure', described_class::FEATURE_ERROR_MESSAGE + end + + context 'without a bug label' do + let(:bug_label_present) { false } + + it_behaves_like 'with a failure', described_class::BUG_ERROR_MESSAGE + end + + context 'when not an applicable version' do + let(:target_branch) { '14-9-stable-ee' } + + it_behaves_like 'with a failure', described_class::VERSION_ERROR_MESSAGE + end + + context 'when the version API request fails' do + let(:response_success) { false } + + it 'adds a warning' do + expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE) + + subject + end + end + + context 'when more than one page of versions is needed' do + # we target a version we know will not be returned in the first request + let(:target_branch) { '14-10-stable-ee' } + + let(:first_version_response) do + instance_double( + HTTParty::Response, + success?: response_success, + parsed_response: [ + { 'version' => '15.1.1' }, + { 'version' => '15.1.0' }, + { 'version' => '15.0.2' }, + { 'version' => '15.0.1' } + ] + ) + end + + let(:second_version_response) do + instance_double( + HTTParty::Response, + success?: response_success, + parsed_response: [ + { 'version' => '15.0.0' }, + { 'version' => '14.10.3' }, + { 'version' => '14.10.2' }, + { 'version' => '14.9.3' } + ] + ) + end + + before do + allow(HTTParty).to receive(:get).with(/page=1/).and_return(first_version_response) + allow(HTTParty).to receive(:get).with(/page=2/).and_return(second_version_response) + end + + it_behaves_like "without a failure" + end + + context 'when too many version API requests are made' do + let(:parsed_response) { [{ 'version' => '15.0.0' }] } + + it 'adds a warning' do + expect(HTTParty).to receive(:get).and_return(version_response).at_least(10).times + expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE) + + subject + end + end + end + end +end diff --git a/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb b/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb new file mode 100644 index 00000000000..eded07c7a2f --- /dev/null +++ b/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Export::PruneProjectExportJobsWorker, feature_category: :importers do + let_it_be(:old_job_1) { create(:project_export_job, updated_at: 37.months.ago) } + let_it_be(:old_job_2) { create(:project_export_job, updated_at: 12.months.ago) } + let_it_be(:old_job_3) { create(:project_export_job, updated_at: 8.days.ago) } + let_it_be(:fresh_job_1) { create(:project_export_job, updated_at: 1.day.ago) } + let_it_be(:fresh_job_2) { create(:project_export_job, updated_at: 2.days.ago) } + let_it_be(:fresh_job_3) { create(:project_export_job, updated_at: 6.days.ago) } + + let_it_be(:old_relation_export_1) { create(:project_relation_export, project_export_job_id: old_job_1.id) } + let_it_be(:old_relation_export_2) { create(:project_relation_export, project_export_job_id: old_job_2.id) } + let_it_be(:old_relation_export_3) { create(:project_relation_export, project_export_job_id: old_job_3.id) } + let_it_be(:fresh_relation_export_1) { create(:project_relation_export, project_export_job_id: fresh_job_1.id) } + + let_it_be(:old_upload_1) { create(:relation_export_upload, project_relation_export_id: old_relation_export_1.id) } + let_it_be(:old_upload_2) { create(:relation_export_upload, project_relation_export_id: old_relation_export_2.id) } + let_it_be(:old_upload_3) { create(:relation_export_upload, project_relation_export_id: old_relation_export_3.id) } + let_it_be(:fresh_upload_1) { create(:relation_export_upload, project_relation_export_id: fresh_relation_export_1.id) } + + subject(:worker) { described_class.new } + + describe '#perform' do + include_examples 'an idempotent worker' do + it 'prunes jobs and associations older than 7 days' do + expect { perform_multiple }.to change { ProjectExportJob.count }.by(-3) + expect(ProjectExportJob.find_by(id: old_job_1.id)).to be_nil + expect(ProjectExportJob.find_by(id: old_job_2.id)).to be_nil + expect(ProjectExportJob.find_by(id: old_job_3.id)).to be_nil + + expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_1.id)).to be_nil + expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_2.id)).to be_nil + expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_3.id)).to be_nil + + expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_1.id)).to be_nil + expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_2.id)).to be_nil + expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_3.id)).to be_nil + end + + it 'leaves fresh jobs and associations' do + perform_multiple + expect(fresh_job_1.reload).to be_present + expect(fresh_job_2.reload).to be_present + expect(fresh_job_3.reload).to be_present + expect(fresh_relation_export_1.reload).to be_present + expect(fresh_upload_1.reload).to be_present + end + end + end +end diff --git a/tooling/danger/stable_branch.rb b/tooling/danger/stable_branch.rb new file mode 100644 index 00000000000..6c0b94b4f06 --- /dev/null +++ b/tooling/danger/stable_branch.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +module Tooling + module Danger + module StableBranch + VersionApiError = Class.new(StandardError) + + STABLE_BRANCH_REGEX = %r{\A(?<version>\d+-\d+)-stable-ee\z}.freeze + + # rubocop:disable Lint/MixedRegexpCaptureTypes + VERSION_REGEX = %r{ + \A(?<major>\d+) + \.(?<minor>\d+) + (\.(?<patch>\d+))? + (-(?<rc>rc(?<rc_number>\d*)))? + (-\h+\.\h+)? + (-ee|\.ee\.\d+)?\z + }x.freeze + # rubocop:enable Lint/MixedRegexpCaptureTypes + + MAINTENANCE_POLICY_URL = 'https://docs.gitlab.com/ee/policy/maintenance.html' + + MAINTENANCE_POLICY_MESSAGE = <<~MSG + See the [release and maintenance policy](#{MAINTENANCE_POLICY_URL}) for more information. + MSG + + FEATURE_ERROR_MESSAGE = <<~MSG + This MR includes the `type::feature` label. Features do not qualify for patch releases. #{MAINTENANCE_POLICY_MESSAGE} + MSG + + BUG_ERROR_MESSAGE = <<~MSG + This branch is meant for backporting bug fixes. If this MR qualifies please add the `type::bug` label. #{MAINTENANCE_POLICY_MESSAGE} + MSG + + VERSION_ERROR_MESSAGE = <<~MSG + Patches are only being accepted on the most recent 3 minor versions of GitLab. #{MAINTENANCE_POLICY_MESSAGE} + MSG + + FAILED_VERSION_REQUEST_MESSAGE = <<~MSG + There was a problem checking if this is a qualified version for backporting. Re-running this job may fix the problem. + MSG + + # rubocop:disable Style/SignalException + def check! + return unless stable_target_branch && !helper.security_mr? + + fail FEATURE_ERROR_MESSAGE if has_feature_label? + fail BUG_ERROR_MESSAGE unless has_bug_label? + fail VERSION_ERROR_MESSAGE unless targeting_patchable_version? + end + # rubocop:enable Style/SignalException + + private + + def stable_target_branch + helper.mr_target_branch.match(STABLE_BRANCH_REGEX) + end + + def has_feature_label? + helper.mr_has_labels?('type::feature') + end + + def has_bug_label? + helper.mr_has_labels?('type::bug') + end + + def targeting_patchable_version? + raise VersionApiError if last_three_minor_versions.empty? + + last_three_minor_versions.include?(targeted_version) + rescue VersionApiError + # don't fail the job since we do not know the recent versions + warn FAILED_VERSION_REQUEST_MESSAGE + true + end + + def last_three_minor_versions + return [] unless versions + + current_version = versions.first.match(VERSION_REGEX) + version_1 = previous_minor_version(current_version) + version_2 = previous_minor_version(version_1) + + [ + version_to_minor_string(current_version), + version_to_minor_string(version_1), + version_to_minor_string(version_2) + ] + end + + def targeted_version + stable_target_branch[1].tr('-', '.') + end + + def versions(page = 1) + version_api_endpoint = "https://version.gitlab.com/api/v1/versions?per_page=50&page=#{page}" + response = HTTParty.get(version_api_endpoint) # rubocop:disable Gitlab/HTTParty + + raise VersionApiError unless response.success? + + version_list = response.parsed_response.map { |v| v['version'] } # rubocop:disable Rails/Pluck + + version_list.sort_by { |v| Gem::Version.new(v) }.reverse + end + + def previous_minor_version(version) + previous_minor = version[:minor].to_i - 1 + + return "#{version[:major]}.#{previous_minor}".match(VERSION_REGEX) if previous_minor >= 0 + + fetch_last_minor_version_for_major(version[:major].to_i - 1) + end + + def fetch_last_minor_version_for_major(major) + page = 1 + last_minor_version = nil + + while last_minor_version.nil? + last_minor_version = versions(page).find do |version| + version.split('.').first.to_i == major + end + + break if page > 10 + + page += 1 + end + + raise VersionApiError if last_minor_version.nil? + + last_minor_version.match(VERSION_REGEX) + end + + def version_to_minor_string(version) + "#{version[:major]}.#{version[:minor]}" + end + end + end +end |