diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-19 18:08:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-19 18:08:32 +0300 |
commit | 846dc476d835e43b123e0d66da3a60ed07f10641 (patch) | |
tree | 02231005811495589ab8bf4a2f4d52f80dc95117 | |
parent | 2ef0b7f13d72eee215e3491a8db4623cbcdd845c (diff) |
Add latest changes from gitlab-org/gitlab@master
64 files changed, 878 insertions, 301 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c62de7d7ab0..ae2e054d714 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,6 +93,7 @@ variables: CHECK_PRECOMPILED_ASSETS: "true" FF_USE_FASTZIP: "true" SKIP_FLAKY_TESTS_AUTOMATICALLY: "true" + RETRY_FAILED_TESTS_IN_NEW_PROCESS: "true" # Run with decomposed databases by default DECOMPOSED_DB: "true" diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index c80e5e56f93..5dd2f9ec06b 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -13.25.0 +13.25.1 diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index cfe35d26b94..7ffe8140a21 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,12 +1,20 @@ <script> -import { GlDropdown, GlTooltipDirective, GlIcon, GlLink, GlSprintf, GlBadge } from '@gitlab/ui'; +import { + GlDropdown, + GlTooltipDirective, + GlIcon, + GlLink, + GlSprintf, + GlBadge, + GlAvatar, + GlAvatarLink, +} from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { __, s__, sprintf } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CommitComponent from '~/vue_shared/components/commit.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import eventHub from '../event_hub'; import ActionsComponent from './environment_actions.vue'; @@ -41,7 +49,8 @@ export default { StopComponent, TerminalButtonComponent, TooltipOnTruncate, - UserAvatarLink, + GlAvatar, + GlAvatarLink, CiIcon, }, directives: { @@ -649,22 +658,27 @@ export default { class="table-section deployment-column d-none d-md-block" :class="tableData.deploy.spacing" role="gridcell" - data-testid="enviornment-deployment-id-cell" + data-testid="environment-deployment-id-cell" > <span v-if="shouldRenderDeploymentID" class="text-break-word"> {{ deploymentInternalId }} </span> - <span v-if="!isFolder && deploymentHasUser" class="text-break-word"> + <span + v-if="!isFolder && deploymentHasUser" + class="text-break-word gl-display-inline-flex gl-align-items-center" + > <gl-sprintf :message="s__('Environments|by %{avatar}')"> <template #avatar> - <user-avatar-link - :link-href="deploymentUser.web_url" - :img-src="deploymentUser.avatar_url" - :img-alt="userImageAltDescription" - :tooltip-text="deploymentUser.username" - class="js-deploy-user-container float-none" - /> + <gl-avatar-link :href="deploymentUser.web_url" class="gl-ml-2"> + <gl-avatar + :src="deploymentUser.avatar_url" + :entity-name="deploymentUser.username" + :title="deploymentUser.username" + :alt="userImageAltDescription" + :size="24" + /> + </gl-avatar-link> </template> </gl-sprintf> </span> @@ -753,20 +767,24 @@ export default { <ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" /> </gl-link> </div> - <div class="gl-display-flex"> - <span v-if="upcomingDeployment.user" class="text-break-word"> - <gl-sprintf :message="s__('Environments|by %{avatar}')"> - <template #avatar> - <user-avatar-link - :link-href="upcomingDeployment.user.web_url" - :img-src="upcomingDeployment.user.avatar_url" - :img-alt="upcomingDeploymentUserImageAltDescription" - :tooltip-text="upcomingDeployment.user.username" + <span + v-if="upcomingDeployment.user" + class="text-break-word gl-display-inline-flex gl-align-items-center gl-mt-2" + > + <gl-sprintf :message="s__('Environments|by %{avatar}')"> + <template #avatar> + <gl-avatar-link :href="upcomingDeployment.user.web_url" class="gl-ml-2"> + <gl-avatar + :src="upcomingDeployment.user.avatar_url" + :alt="upcomingDeploymentUserImageAltDescription" + :entity-name="upcomingDeployment.user.username" + :title="upcomingDeployment.user.username" + :size="24" /> - </template> - </gl-sprintf> - </span> - </div> + </gl-avatar-link> + </template> + </gl-sprintf> + </span> </div> </div> diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index 6c8cf84cbaa..4406cacdf3f 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -112,6 +112,9 @@ export default { count: this.searchOptions.length, }); }, + headerSearchActivityDescriptor() { + return this.showDropdown ? 'is-active' : 'is-not-active'; + }, }, methods: { ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']), @@ -143,7 +146,8 @@ export default { v-outside="closeDropdown" role="search" :aria-label="$options.i18n.searchGitlab" - class="header-search gl-relative" + class="header-search gl-relative gl-rounded-base" + :class="headerSearchActivityDescriptor" > <gl-search-box-by-type id="search" diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index ae201a61db6..0714d88b392 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -3,7 +3,7 @@ import { GlButton } from '@gitlab/ui'; import api from '~/api'; import { __ } from '~/locale'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; -import Popover from '~/vue_shared/components/help_popover.vue'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants'; import IssuesList from './issues_list.vue'; @@ -13,7 +13,7 @@ export default { components: { GlButton, IssuesList, - Popover, + HelpPopover, StatusIcon, }, mixins: [glFeatureFlagsMixin()], @@ -193,7 +193,7 @@ export default { <div class="gl-display-flex gl-align-items-center"> <p class="gl-line-height-normal gl-m-0">{{ headerText }}</p> <slot :name="slotName"></slot> - <popover + <help-popover v-if="hasPopover" :options="popoverOptions" class="gl-ml-2 gl-display-inline-flex" diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index c84a83c1fab..18a0f119edf 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -45,6 +45,36 @@ input[type='checkbox']:hover { transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration; } + + &.is-not-active { + .btn.gl-clear-icon-button { + display: none; + } + + &::after { + content: '/'; + display: inline-block; + position: absolute; + top: 0; + right: 8px; + transform: translateY(calc(50% - 4px)); + padding: 4px 5px; + font-size: $gl-font-size-small; + font-family: $monospace-font; + line-height: 1; + vertical-align: middle; + border-width: 0; + border-style: solid; + border-image: none; + border-radius: 3px; + box-shadow: none; + white-space: pre-wrap; + // Safari + word-wrap: break-word; + overflow-wrap: break-word; + word-break: keep-all; + } + } } .header-search-dropdown-menu { diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 2796748f022..62d45332204 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -1516,6 +1516,29 @@ svg.s16 { .header-search { width: 320px; } +.header-search.is-not-active::after { + content: "/"; + display: inline-block; + position: absolute; + top: 0; + right: 8px; + transform: translateY(calc(50% - 4px)); + padding: 4px 5px; + font-size: 12px; + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", + "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + line-height: 1; + vertical-align: middle; + border-width: 0; + border-style: solid; + border-image: none; + border-radius: 3px; + box-shadow: none; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: keep-all; +} .search { margin: 0 8px; } @@ -1854,6 +1877,10 @@ body.gl-dark .header-search input::placeholder { body.gl-dark .header-search input:active::placeholder { color: #868686; } +body.gl-dark .header-search.is-not-active::after { + color: #fafafa; + background-color: rgba(250, 250, 250, 0.2); +} body.gl-dark .search form { background-color: rgba(250, 250, 250, 0.2); } diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 8106f603813..a8b7299b935 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1502,6 +1502,29 @@ svg.s16 { .header-search { width: 320px; } +.header-search.is-not-active::after { + content: "/"; + display: inline-block; + position: absolute; + top: 0; + right: 8px; + transform: translateY(calc(50% - 4px)); + padding: 4px 5px; + font-size: 12px; + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", + "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + line-height: 1; + vertical-align: middle; + border-width: 0; + border-style: solid; + border-image: none; + border-radius: 3px; + box-shadow: none; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: keep-all; +} .search { margin: 0 8px; } diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss index c6e29c7f8b0..07194e2b532 100644 --- a/app/assets/stylesheets/themes/theme_helper.scss +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -176,6 +176,11 @@ } } } + + &.is-not-active::after { + color: $search-and-nav-links; + background-color: rgba($search-and-nav-links, 0.2); + } } .search { diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 4bcd1be9f53..663e3cf8648 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -13,6 +13,7 @@ class AutocompleteController < ApplicationController feature_category :continuous_delivery, [:deploy_keys_with_owners] urgency :low, [:merge_request_target_branches] + urgency :default, [:users] def users group = Autocomplete::GroupFinder diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index e50369e5f8e..23e0143506e 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -25,6 +25,9 @@ class Explore::ProjectsController < Explore::ApplicationController feature_category :projects + # TODO: Set higher urgency after addressing https://gitlab.com/gitlab-org/gitlab/-/issues/357913 + urgency :low, [:index] + def index show_alert_if_search_is_disabled @projects = load_projects diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb index 10a6ad06ae5..d10c52f0301 100644 --- a/app/controllers/groups/children_controller.rb +++ b/app/controllers/groups/children_controller.rb @@ -9,6 +9,9 @@ module Groups feature_category :subgroups + # TODO: Set to higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/331494 + urgency :low, [:index] + def index params[:sort] ||= @group_projects_sort parent = if params[:parent_id].present? diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 617d22eb768..51778f31f65 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -20,7 +20,7 @@ class Groups::GroupMembersController < Groups::ApplicationController :approve_access_request, :leave, :resend_invite, :override - feature_category :authentication_and_authorization + feature_category :subgroups def index push_frontend_feature_flag(:group_member_inherited_group, @group) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index e5d793b1099..995d5abf045 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -59,7 +59,8 @@ class GroupsController < Groups::ApplicationController feature_category :importers, [:export, :download_export] urgency :high, [:unfoldered_environment_names] - urgency :low, [:merge_requests] + # TODO: Set #show to higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/334795 + urgency :low, [:merge_requests, :show] def index redirect_to(current_user ? dashboard_groups_path : explore_groups_path) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 010b85e81bf..8eebf9fbf6b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -9,6 +9,8 @@ class JwtController < ApplicationController prepend_before_action :auth_user, :authenticate_project_or_user feature_category :authentication_and_authorization + # https://gitlab.com/gitlab-org/gitlab/-/issues/357037 + urgency :low SERVICES = { ::Auth::ContainerRegistryAuthenticationService::AUDIENCE => ::Auth::ContainerRegistryAuthenticationService, diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 0279a65f262..49618c89672 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -8,7 +8,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] - feature_category :authentication_and_authorization + feature_category :projects def index @sort = params[:sort].presence || sort_value_name diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8597c0b2432..6cdfdfa9e2f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -56,7 +56,8 @@ class ProjectsController < Projects::ApplicationController feature_category :code_review, [:unfoldered_environment_names] feature_category :portfolio_management, [:planning_hierarchy] - urgency :low, [:refs] + # TODO: Set high urgency for #show https://gitlab.com/gitlab-org/gitlab/-/issues/334444 + urgency :low, [:refs, :show] urgency :high, [:unfoldered_environment_names] def index diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e6d9dae5989..228ef710749 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -33,6 +33,9 @@ class UsersController < ApplicationController feature_category :snippets, [:snippets] + # TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357914 + urgency :low, [:show] + def show respond_to do |format| format.html diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index 96120d9412f..64903c67573 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -73,10 +73,20 @@ class UserRecentEventsFinder return Event.none if users.empty? - if event_filter.filter == EventFilter::ALL - execute_optimized_multi(users) + if Feature.enabled?(:optimized_followed_users_queries, current_user) + query_builder_params = event_filter.in_operator_query_builder_params(users) + + Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder + .new(**query_builder_params) + .execute + .limit(limit) + .offset(params[:offset] || 0) else - event_filter.apply_filter(Event.where(author: users).limit_recent(limit, params[:offset] || 0)) + if event_filter.filter == EventFilter::ALL + execute_optimized_multi(users) + else + event_filter.apply_filter(Event.where(author: users).limit_recent(limit, params[:offset] || 0)) + end end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 57ece59b3fc..22e5436dc5c 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -19,6 +19,8 @@ class AwardEmoji < ApplicationRecord participant :user + delegate :resource_parent, to: :awardable, allow_nil: true + scope :downvotes, -> { named(DOWNVOTE_NAME) } scope :upvotes, -> { named(UPVOTE_NAME) } scope :named, -> (names) { where(name: names) } @@ -61,7 +63,9 @@ class AwardEmoji < ApplicationRecord end def url - awardable.try(:namespace)&.custom_emoji&.by_name(name)&.first&.url + return if TanukiEmoji.find_by_alpha_code(name) + + CustomEmoji.for_resource(resource_parent).by_name(name).select(:url).first&.url end def expire_cache diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 173b38b2c63..09fbb93525b 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -28,6 +28,19 @@ class CustomEmoji < ApplicationRecord alias_attribute :url, :file # this might need a change in https://gitlab.com/gitlab-org/gitlab/-/issues/230467 + # Find custom emoji for the given resource. + # A resource can be either a Project or a Group, or anything responding to #root_ancestor. + # Usually it's the return value of #resource_parent on any model. + scope :for_resource, -> (resource) do + return none if resource.nil? + + namespace = resource.root_ancestor + + return none if namespace.nil? || Feature.disabled?(:custom_emoji, namespace) + + namespace.custom_emoji + end + private def valid_emoji_name diff --git a/app/models/event.rb b/app/models/event.rb index 38740dc89d6..e9a98c06b59 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -31,8 +31,9 @@ class Event < ApplicationRecord private_constant :ACTIONS WIKI_ACTIONS = [:created, :updated, :destroyed].freeze - DESIGN_ACTIONS = [:created, :updated, :destroyed].freeze + TEAM_ACTIONS = [:joined, :left, :expired].freeze + ISSUE_ACTIONS = [:created, :updated, :closed, :reopened].freeze TARGET_TYPES = HashWithIndifferentAccess.new( issue: Issue, diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 38aaeff5c9a..cf4b83d44c2 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -40,6 +40,7 @@ class Snippet < ApplicationRecord belongs_to :author, class_name: 'User' belongs_to :project + alias_method :resource_parent, :project has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :user_mentions, class_name: "SnippetUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/validators/gitlab/emoji_name_validator.rb b/app/validators/gitlab/emoji_name_validator.rb index c14d6e4ec78..c034a79214b 100644 --- a/app/validators/gitlab/emoji_name_validator.rb +++ b/app/validators/gitlab/emoji_name_validator.rb @@ -24,11 +24,9 @@ module Gitlab end def valid_custom_emoji?(record, value) - namespace = record.try(:awardable).try(:namespace) + resource = record.try(:resource_parent) - return unless namespace - - namespace.custom_emoji&.by_name(value.to_s)&.any? + CustomEmoji.for_resource(resource).by_name(value.to_s).any? end end end diff --git a/app/views/layouts/_header_search.html.haml b/app/views/layouts/_header_search.html.haml index 6cac37f112e..f7b7aac6de4 100644 --- a/app/views/layouts/_header_search.html.haml +++ b/app/views/layouts/_header_search.html.haml @@ -1,4 +1,4 @@ -#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json, +#js-header-search.header-search.is-not-active.gl-relative{ data: { 'search-context' => header_search_context.to_json, 'search-path' => search_path, 'issues-path' => issues_dashboard_path, 'mr-path' => merge_requests_dashboard_path, diff --git a/config/feature_flags/development/optimized_followed_users_queries.yml b/config/feature_flags/development/optimized_followed_users_queries.yml new file mode 100644 index 00000000000..514c3c91829 --- /dev/null +++ b/config/feature_flags/development/optimized_followed_users_queries.yml @@ -0,0 +1,8 @@ +--- +name: optimized_followed_users_queries +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84856 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358649 +milestone: '14.10' +type: development +group: group::optimize +default_enabled: false diff --git a/db/post_migrate/20220409160628_add_async_index_for_events_followed_users.rb b/db/post_migrate/20220409160628_add_async_index_for_events_followed_users.rb new file mode 100644 index 00000000000..fb858248b19 --- /dev/null +++ b/db/post_migrate/20220409160628_add_async_index_for_events_followed_users.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddAsyncIndexForEventsFollowedUsers < Gitlab::Database::Migration[1.0] + INDEX_NAME = 'index_events_for_followed_users' + + def up + prepare_async_index :events, %I[author_id target_type action id], name: INDEX_NAME + end + + def down + unprepare_async_index :events, %I[author_id target_type action id], name: INDEX_NAME + end +end diff --git a/db/schema_migrations/20220409160628 b/db/schema_migrations/20220409160628 new file mode 100644 index 00000000000..29b46427dd2 --- /dev/null +++ b/db/schema_migrations/20220409160628 @@ -0,0 +1 @@ +7952024a6a8df98842fa23ca9a4c328b83816ded3071e7597dbab431a5561e1a
\ No newline at end of file diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md index fc6e81604cb..75408de2721 100644 --- a/doc/ci/pipelines/merge_request_pipelines.md +++ b/doc/ci/pipelines/merge_request_pipelines.md @@ -20,7 +20,7 @@ Branch pipelines: - Run when you push a new commit to a branch. - Are the default type of pipeline. - Have access to [some predefined variables](../variables/predefined_variables.md). -- Have access to [protected variables](../variables/index.md#protect-a-cicd-variable) and [protected runners](../runners/configure_runners.md#prevent-runners-from-revealing-sensitive-information. +- Have access to [protected variables](../variables/index.md#protect-a-cicd-variable) and [protected runners](../runners/configure_runners.md#prevent-runners-from-revealing-sensitive-information). Merge request pipelines: diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 2aef0e10314..e0b236bc5fc 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -187,7 +187,7 @@ See the [experiment issue](https://gitlab.com/gitlab-org/quality/team-tasks/-/is #### Automatic retry of failing tests in a separate process -When the `$RETRY_FAILED_TESTS_IN_NEW_PROCESS` variable is set to `true`, RSpec tests that failed are automatically retried once in a separate +Unless `$RETRY_FAILED_TESTS_IN_NEW_PROCESS` variable is set to `false` (`true` by default), RSpec tests that failed are automatically retried once in a separate RSpec process. The goal is to get rid of most side-effects from previous tests that may lead to a subsequent test failure. We keep track of retried tests in the `$RETRIED_TESTS_REPORT_FILE` file saved as artifact by the `rspec:flaky-tests-report` job. diff --git a/doc/integration/jira/configure.md b/doc/integration/jira/configure.md index 2033ddbad6f..bfeac230f89 100644 --- a/doc/integration/jira/configure.md +++ b/doc/integration/jira/configure.md @@ -22,7 +22,8 @@ Prerequisites: To configure your project: -1. Go to your project and select [**Settings > Integrations**](../../user/project/integrations/overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Jira**. 1. Select **Enable integration**. 1. Select **Trigger** actions. Your choice determines whether a mention of Jira issue diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index 22367435ae4..4537faaaead 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -199,6 +199,17 @@ cd /home/git/gitlab git diff origin/PREVIOUS_BRANCH:config/gitlab.yml.example origin/BRANCH:config/gitlab.yml.example ``` +#### New configuration options for `database.yml` + +There might be configuration options available for [`database.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/database.yml.postgresql). +View them with the command below and apply them manually to your current `database.yml`: + +```shell +cd /home/git/gitlab + +git diff origin/PREVIOUS_BRANCH:config/database.yml.postgresql origin/BRANCH:config/database.yml.postgresql +``` + #### NGINX configuration Ensure you're still up-to-date with the latest NGINX configuration changes: diff --git a/doc/user/project/integrations/asana.md b/doc/user/project/integrations/asana.md index b4d7790df1d..a10e261f10e 100644 --- a/doc/user/project/integrations/asana.md +++ b/doc/user/project/integrations/asana.md @@ -32,8 +32,8 @@ In Asana, create a Personal Access Token. Complete these steps in GitLab: -1. Go to the project you want to configure. -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Asana**. 1. Ensure that the **Active** toggle is enabled. 1. Paste the token you generated in Asana. diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index a54a3adc408..4a9a8d62098 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -14,7 +14,8 @@ You can configure Bugzilla as an To enable the Bugzilla integration in a project: -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Bugzilla**. 1. Select the checkbox under **Enable integration**. 1. Fill in the required fields: diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index ad7719f0e5b..b7e25b815fc 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -26,8 +26,9 @@ and configure it in GitLab. With the webhook URL created in the Discord channel, you can set up the Discord Notifications service in GitLab. -1. Navigate to the [Integrations page](overview.md#accessing-integrations) in your project's settings. That is, **Project > Settings > Integrations**. -1. Select the **Discord Notifications** integration to configure it. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **Discord Notifications**. 1. Ensure that the **Active** toggle is enabled. 1. Check the checkboxes corresponding to the GitLab events for which you want to send notifications to Discord. 1. Paste the webhook URL that you copied from the create Discord webhook step. diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index 33c197b962e..c1c48c7fb12 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -9,17 +9,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w By enabling this service, you receive email notifications for every change that is pushed to your project. -From the [Integrations page](overview.md#accessing-integrations) -select **Emails on push** service to activate and configure it. +To enable emails on push: -In the _Recipients_ area, provide a list of emails separated by spaces or newlines. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **Emails on push**. +1. In the **Recipients** section, provide a list of emails separated by spaces or newlines. +1. Configure the following options: -The following options are available: - -- **Push events** - Email is triggered when a push event is received. -- **Tag push events** - Email is triggered when a tag is created and pushed. -- **Send from committer** - Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as `user@gitlab.com`). -- **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. + - **Push events** - Email is triggered when a push event is received. + - **Tag push events** - Email is triggered when a tag is created and pushed. + - **Send from committer** - Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as `user@gitlab.com`). + - **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. | Settings | Notification | | --- | --- | diff --git a/doc/user/project/integrations/ewm.md b/doc/user/project/integrations/ewm.md index bc9b2d59db3..b02f1a06e96 100644 --- a/doc/user/project/integrations/ewm.md +++ b/doc/user/project/integrations/ewm.md @@ -14,7 +14,8 @@ This IBM product was [formerly named Rational Team Concert](https://jazz.net/blo To enable the EWM integration, in a project: -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **EWM**. 1. Select the checkbox under **Enable integration**. 1. Fill in the required fields: diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 279b139bacd..b2c2aea2c2b 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -39,9 +39,8 @@ network. For more details, read ## Complete these steps in GitLab -1. On the top bar, select **Menu > Projects** and find the project you want to - configure for notifications. -1. Navigate to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **irker (IRC gateway)**. 1. Ensure that the **Active** toggle is enabled. 1. Optional. Under **Server host**, enter the server host address where `irkerd` runs. If empty, diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index f3f8d900e12..7dd4c1d1a8b 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -37,27 +37,25 @@ Display name override is not enabled by default, you need to ask your administra ## Configure GitLab to send notifications to Mattermost After the Mattermost instance has an incoming webhook set up, you can set up GitLab -to send the notifications. - -Navigate to the [Integrations page](overview.md#accessing-integrations) -and select the **Mattermost notifications** service. Select the GitLab events -you want to generate notifications for. - -For each event you select, input the Mattermost channel you want to receive the -notification. You do not need to add the hash sign (`#`). - -Then fill in the integration configuration: - -- **Webhook**: The incoming webhook URL on Mattermost, similar to - `http://mattermost.example/hooks/5xo…`. -- **Username**: Optional. The username shown in messages sent to Mattermost. - To change the bot's username, provide a value. -- **Notify only broken pipelines**: If you enable the **Pipeline** event, and you want - notifications about failed pipelines only. -- **Branches for which notifications are to be sent**: The branches to send notifications for. -- **Labels to be notified**: Optional. Labels required for the issue or merge request - to trigger a notification. Leave blank to notify for all issues and merge requests. -- **Labels to be notified behavior**: When you use the **Labels to be notified** filter, - messages are sent when an issue or merge request contains _any_ of the labels specified - in the filter. You can also choose to trigger messages only when the issue or merge request - contains _all_ the labels defined in the filter. +to send the notifications: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **Mattermost notifications**. +1. Select the GitLab events to generate notifications for. For each event you select, input the Mattermost channel + to receive the notification. You do not need to add the hash sign (`#`). +1. Fill in the integration configuration: + + - **Webhook**: The incoming webhook URL on Mattermost, similar to + `http://mattermost.example/hooks/5xo…`. + - **Username**: Optional. The username shown in messages sent to Mattermost. + To change the bot's username, provide a value. + - **Notify only broken pipelines**: If you enable the **Pipeline** event, and you want + notifications about failed pipelines only. + - **Branches for which notifications are to be sent**: The branches to send notifications for. + - **Labels to be notified**: Optional. Labels required for the issue or merge request + to trigger a notification. Leave blank to notify for all issues and merge requests. + - **Labels to be notified behavior**: When you use the **Labels to be notified** filter, + messages are sent when an issue or merge request contains _any_ of the labels specified + in the filter. You can also choose to trigger messages only when the issue or merge request + contains _all_ the labels defined in the filter. diff --git a/doc/user/project/integrations/pivotal_tracker.md b/doc/user/project/integrations/pivotal_tracker.md index 8b17f4afaa8..7f5414b86de 100644 --- a/doc/user/project/integrations/pivotal_tracker.md +++ b/doc/user/project/integrations/pivotal_tracker.md @@ -37,8 +37,8 @@ In Pivotal Tracker, [create an API token](https://www.pivotaltracker.com/help/ar Complete these steps in GitLab: -1. Go to the project you want to configure. -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Pivotal Tracker**. 1. Ensure that the **Active** toggle is enabled. 1. Paste the token you generated in Pivotal Tracker. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 760b5030416..068a2810a53 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -62,9 +62,9 @@ GitLab can use these to access the resource. More information about authenticati service account can be found at Google's documentation for [Authenticating from a service account](https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account). -1. Navigate to the [Integrations page](overview.md#accessing-integrations) at - **Settings > Integrations**. -1. Click the **Prometheus** service. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **Prometheus**. 1. For **API URL**, provide the domain name or IP address of your server, such as `http://prometheus.example.com/` or `http://192.0.2.1/`. 1. (Optional) In **Google IAP Audience Client ID**, provide the Client ID of the @@ -73,7 +73,7 @@ service account can be found at Google's documentation for Service Account credentials file that is authorized to access the Prometheus resource. The JSON key `token_credential_uri` is discarded to prevent [Server-side Request Forgery (SSRF)](https://www.hackerone.com/application-security/how-server-side-request-forgery-ssrf). -1. Click **Save changes**. +1. Select **Save changes**. ![Configure Prometheus Service](img/prometheus_manual_configuration_v13_2.png) @@ -83,11 +83,12 @@ You can configure [Thanos](https://thanos.io/) as a drop-in replacement for Prom with GitLab. Use the domain name or IP address of the Thanos server you'd like to integrate with. -1. Navigate to the [Integrations page](overview.md#accessing-integrations). -1. Click the **Prometheus** service. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **Prometheus**. 1. Provide the domain name or IP address of your server, for example `http://thanos.example.com/` or `http://192.0.2.1/`. -1. Click **Save changes**. +1. Select **Save changes**. ### Precedence with multiple Prometheus configurations diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 05d7c31a288..bcab8d05f69 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -10,7 +10,8 @@ Use [Redmine](https://www.redmine.org/) as the issue tracker. To enable the Redmine integration in a project: -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Redmine**. 1. Select the checkbox under **Enable integration**. 1. Fill in the required fields: diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index cddb72a83b2..5ad344a7d8e 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -18,7 +18,7 @@ For GitLab.com, use the [GitLab Slack app](gitlab_slack_application.md) instead. ## Configure GitLab and Slack -Slack slash command [integrations](overview.md#accessing-integrations) +Slack slash command integrations are scoped to a project. 1. In GitLab, on the top bar, select **Menu > Projects** and find your project. diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md index daab24a8ab9..1e607d89e80 100644 --- a/doc/user/project/integrations/unify_circuit.md +++ b/doc/user/project/integrations/unify_circuit.md @@ -15,7 +15,8 @@ copy its URL. In GitLab: -1. Go to the [Integrations page](overview.md#accessing-integrations) in your project's settings. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **Unify Circuit**. 1. Turn on the **Active** toggle. 1. Select the checkboxes corresponding to the GitLab events you want to receive in Unify Circuit. diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index eda0874ac08..6c70a5e679b 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -14,7 +14,8 @@ You can configure YouTrack as an To enable the YouTrack integration in a project: -1. Go to the [Integrations page](overview.md#accessing-integrations). +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. 1. Select **YouTrack**. 1. Select the checkbox under **Enable integration**. 1. Fill in the required fields: diff --git a/lib/api/groups.rb b/lib/api/groups.rb index af4722f8463..0ed14476c61 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -249,7 +249,8 @@ module API use :with_custom_attributes optional :with_projects, type: Boolean, default: true, desc: 'Omit project details' end - get ":id", feature_category: :subgroups do + # TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357841 + get ":id", feature_category: :subgroups, urgency: :low do group = find_group!(params[:id]) group.preload_shared_group_links @@ -300,7 +301,8 @@ module API use :with_custom_attributes use :optional_projects_params end - get ":id/projects", feature_category: :subgroups do + # TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/211498 + get ":id/projects", feature_category: :subgroups, urgency: :low do finder_options = { only_owned: !params[:with_shared], include_subgroups: params[:include_subgroups], @@ -347,7 +349,7 @@ module API use :group_list_params use :with_custom_attributes end - get ":id/subgroups", feature_category: :subgroups do + get ":id/subgroups", feature_category: :subgroups, urgency: :low do groups = find_groups(declared_params(include_missing: false), params[:id]) present_groups params, groups end diff --git a/lib/api/members.rb b/lib/api/members.rb index f236cc89a11..01e859c94c4 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -7,6 +7,7 @@ module API before { authenticate! } feature_category :authentication_and_authorization + urgency :low helpers ::API::Helpers::MembersHelpers diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb index 69b47f9420d..e8829216336 100644 --- a/lib/api/project_events.rb +++ b/lib/api/project_events.rb @@ -8,6 +8,9 @@ module API feature_category :users + # TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357839 + urgency :low + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 71971ee4d8f..9f7b3f9b088 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -214,7 +214,7 @@ module API use :statistics_params use :with_custom_attributes end - get ":user_id/projects", feature_category: :projects do + get ":user_id/projects", feature_category: :projects, urgency: :default do user = find_user(params[:user_id]) not_found!('User') unless user @@ -251,7 +251,8 @@ module API use :statistics_params use :with_custom_attributes end - get feature_category: :projects do + # TODO: Set higher urgency https://gitlab.com/gitlab-org/gitlab/-/issues/211495 + get feature_category: :projects, urgency: :low do present_projects load_projects end @@ -340,7 +341,8 @@ module API optional :license, type: Boolean, default: false, desc: 'Include project license data' end - get ":id", feature_category: :projects do + # TODO: Set higher urgency https://gitlab.com/gitlab-org/gitlab/-/issues/357622 + get ":id", feature_category: :projects, urgency: :default do options = { with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, current_user: current_user, diff --git a/lib/api/users.rb b/lib/api/users.rb index 37ef6a95235..b26611cfe03 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -99,7 +99,7 @@ module API use :optional_index_params_ee end # rubocop: disable CodeReuse/ActiveRecord - get feature_category: :users do + get feature_category: :users, urgency: :default do authenticated_as_admin! if params[:extern_uid].present? && params[:provider].present? unless current_user&.admin? @@ -143,7 +143,7 @@ module API use :with_custom_attributes end # rubocop: disable CodeReuse/ActiveRecord - get ":id", feature_category: :users do + get ":id", feature_category: :users, urgency: :medium do forbidden!('Not authorized!') unless current_user unless current_user.admin? @@ -168,7 +168,7 @@ module API params do requires :user_id, type: String, desc: 'The ID or username of the user' end - get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users do + get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users, urgency: :high do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) @@ -919,7 +919,7 @@ module API desc 'Get the currently authenticated user' do success Entities::UserPublic end - get feature_category: :users do + get feature_category: :users, urgency: :medium do entity = if current_user.admin? Entities::UserWithAdmin diff --git a/lib/banzai/filter/custom_emoji_filter.rb b/lib/banzai/filter/custom_emoji_filter.rb index a5f1a22c483..ae95c7f66b6 100644 --- a/lib/banzai/filter/custom_emoji_filter.rb +++ b/lib/banzai/filter/custom_emoji_filter.rb @@ -8,8 +8,7 @@ module Banzai IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set def call - return doc unless context[:project] - return doc unless Feature.enabled?(:custom_emoji, context[:project]) + return doc unless resource_parent doc.xpath('descendant-or-self::text()').each do |node| content = node.to_html @@ -50,12 +49,12 @@ module Banzai def has_custom_emoji? strong_memoize(:has_custom_emoji) do - namespace&.custom_emoji&.any? + CustomEmoji.for_resource(resource_parent).any? end end - def namespace - context[:project].namespace.root_ancestor + def resource_parent + context[:project] || context[:group] end def custom_emoji_candidates @@ -63,7 +62,8 @@ module Banzai end def all_custom_emoji - @all_custom_emoji ||= namespace.custom_emoji.by_name(custom_emoji_candidates).index_by(&:name) + @all_custom_emoji ||= + CustomEmoji.for_resource(resource_parent).by_name(custom_emoji_candidates).index_by(&:name) end end end diff --git a/lib/event_filter.rb b/lib/event_filter.rb index 915ab355508..8833207dd1d 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop: disable CodeReuse/ActiveRecord class EventFilter include Gitlab::Utils::StrongMemoize @@ -24,7 +25,6 @@ class EventFilter filter == key.to_s end - # rubocop: disable CodeReuse/ActiveRecord def apply_filter(events) case filter when PUSH @@ -34,9 +34,9 @@ class EventFilter when COMMENTS events.commented_action when TEAM - events.where(action: [:joined, :left, :expired]) + events.where(action: Event::TEAM_ACTIONS) when ISSUE - events.where(action: [:created, :updated, :closed, :reopened], target_type: 'Issue') + events.where(action: Event::ISSUE_ACTIONS, target_type: 'Issue') when WIKI wiki_events(events) when DESIGNS @@ -45,10 +45,157 @@ class EventFilter events end end - # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable Metrics/CyclomaticComplexity + # This method build specialized in-operator optimized queries based on different + # filter parameters. All queries will benefit from the index covering the following columns: + # author_id target_type action id + # + # More context: https://docs.gitlab.com/ee/development/database/efficient_in_operator_queries.html#the-inoperatoroptimization-module + def in_operator_query_builder_params(user_ids) + case filter + when ALL + in_operator_params(array_scope_ids: user_ids) + when PUSH + # Here we need to add an order hint column to force the correct index usage. + # Without the order hint, the following conditions will use the `index_events_on_author_id_and_id` + # index which is not as efficient as the `index_events_for_followed_users` index. + # > target_type IS NULL AND action = 5 AND author_id = X ORDER BY id DESC + # + # The order hint adds an extra order by column which doesn't affect the result but forces the planner + # to use the correct index: + # > target_type IS NULL AND action = 5 AND author_id = X ORDER BY target_type DESC, id DESC + in_operator_params( + array_scope_ids: user_ids, + scope: Event.where(target_type: nil).pushed_action, + order_hint_column: :target_type + ) + when MERGED + in_operator_params( + array_scope_ids: user_ids, + scope: Event.where(target_type: MergeRequest.to_s).merged_action + ) + when COMMENTS + in_operator_params( + array_scope_ids: user_ids, + scope: Event.commented_action, + in_column: :target_type, + in_values: [Note, *Note.descendants].map(&:name) # To make the query efficient we need to list all Note classes + ) + when TEAM + in_operator_params( + array_scope_ids: user_ids, + scope: Event.where(target_type: nil), + order_hint_column: :target_type, + in_column: :action, + in_values: Event.actions.values_at(*Event::TEAM_ACTIONS) + ) + when ISSUE + in_operator_params( + array_scope_ids: user_ids, + scope: Event.where(target_type: Issue.name), + in_column: :action, + in_values: Event.actions.values_at(*Event::ISSUE_ACTIONS) + ) + when WIKI + in_operator_params( + array_scope_ids: user_ids, + scope: Event.for_wiki_page, + in_column: :action, + in_values: Event.actions.values_at(*Event::WIKI_ACTIONS) + ) + when DESIGNS + in_operator_params( + array_scope_ids: user_ids, + scope: Event.for_design, + in_column: :action, + in_values: Event.actions.values_at(*Event::DESIGN_ACTIONS) + ) + else + in_operator_params(array_scope_ids: user_ids) + end + end + # rubocop: enable Metrics/CyclomaticComplexity private + def in_operator_params(array_scope_ids:, scope: nil, in_column: nil, in_values: nil, order_hint_column: nil) + base_scope = Event.all + base_scope = base_scope.merge(scope) if scope + + order = { id: :desc } + finder_query = -> (id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) } + + if order_hint_column.present? + order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: order_hint_column, + order_expression: Event.arel_table[order_hint_column].desc, + nullable: :nulls_last, + distinct: false + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Event.arel_table[:id].desc + ) + ]) + + finder_query = -> (_order_hint, id_expression) { Event.where(Event.arel_table[:id].eq(id_expression)) } + end + + base_scope = base_scope.reorder(order) + + array_params = in_operator_array_params( + array_scope_ids: array_scope_ids, + scope: base_scope, + in_column: in_column, + in_values: in_values + ) + + array_params.merge( + scope: base_scope, + finder_query: finder_query + ) + end + + # This method builds the array_ parameters + # without in_column parameter: uses one IN filter: author_id + # with in_column: two IN filters: author_id, (target_type OR action) + def in_operator_array_params(scope:, array_scope_ids:, in_column: nil, in_values: nil) + if in_column + # Builds Carthesian product of the in_values and the array_scope_ids (in this case: user_ids). + # The process is described here: https://docs.gitlab.com/ee/development/database/efficient_in_operator_queries.html#multiple-in-queries + # VALUES ((array_scope_ids[0], in_values[0]), (array_scope_ids[1], in_values[0]) ...) + cartesian = array_scope_ids.product(in_values) + user_with_column_list = Arel::Nodes::ValuesList.new(cartesian) + + as = "array_ids(id, #{Event.connection.quote_column_name(in_column)})" + from = Arel::Nodes::Grouping.new(user_with_column_list).as(as) + { + array_scope: User.select(:id, in_column).from(from), + array_mapping_scope: -> (author_id_expression, in_column_expression) do + Event + .merge(scope) + .where(Event.arel_table[:author_id].eq(author_id_expression)) + .where(Event.arel_table[in_column].eq(in_column_expression)) + end + } + else + # Builds a simple query to represent the array_scope_ids + # VALUES ((array_scope_ids[0]), (array_scope_ids[2])...) + array_ids_list = Arel::Nodes::ValuesList.new(array_scope_ids.map { |id| [id] }) + from = Arel::Nodes::Grouping.new(array_ids_list).as('array_ids(id)') + { + array_scope: User.select(:id).from(from), + array_mapping_scope: -> (author_id_expression) do + Event + .merge(scope) + .where(Event.arel_table[:author_id].eq(author_id_expression)) + end + } + end + end + def wiki_events(events) events.for_wiki_page end @@ -61,5 +208,6 @@ class EventFilter [ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM, WIKI, DESIGNS] end end +# rubocop: enable CodeReuse/ActiveRecord EventFilter.prepend_mod_with('EventFilter') diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 4a7d14af6ae..d016dea224b 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -692,6 +692,8 @@ module Gitlab # batch_column_name - option for tables without a primary key, in this case # another unique integer column can be used. Example: :user_id def undo_cleanup_concurrent_column_type_change(table, column, old_type, type_cast_function: nil, batch_column_name: :id, limit: nil) + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_ddl_mode! + temp_column = "#{column}_for_type_change" # Using a descriptive name that includes orinal column's name risks @@ -1639,7 +1641,9 @@ into similar problems in the future (e.g. when new tables are created). old_value = Arel::Nodes::NamedFunction.new(type_cast_function, [old_value]) end - update_column_in_batches(table, new, old_value, batch_column_name: batch_column_name) + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do + update_column_in_batches(table, new, old_value, batch_column_name: batch_column_name) + end add_not_null_constraint(table, new) unless old_col.null diff --git a/lib/gitlab/database/migration_helpers/v2.rb b/lib/gitlab/database/migration_helpers/v2.rb index 0e7f6075196..dd426962033 100644 --- a/lib/gitlab/database/migration_helpers/v2.rb +++ b/lib/gitlab/database/migration_helpers/v2.rb @@ -134,6 +134,8 @@ module Gitlab # batch_column_name - option is for tables without primary key, in this # case another unique integer column can be used. Example: :user_id def rename_column_concurrently(table, old_column, new_column, type: nil, batch_column_name: :id) + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_ddl_mode! + setup_renamed_column(__callee__, table, old_column, new_column, type, batch_column_name) with_lock_retries do @@ -181,6 +183,8 @@ module Gitlab # case another unique integer column can be used. Example: :user_id # def undo_cleanup_concurrent_column_rename(table, old_column, new_column, type: nil, batch_column_name: :id) + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_ddl_mode! + setup_renamed_column(__callee__, table, new_column, old_column, type, batch_column_name) with_lock_retries do diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb index 065a3a0cf20..8c0f082f61c 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb @@ -120,7 +120,7 @@ module Gitlab .from(array_cte) .join(Arel.sql("LEFT JOIN LATERAL (#{initial_keyset_query.to_sql}) #{table_name} ON TRUE")) - order_by_columns.each { |column| q.where(column.column_expression.not_eq(nil)) } + order_by_columns.each { |c| q.where(c.column_expression.not_eq(nil)) unless c.column.nullable? } q.as('array_scope_lateral_query') end @@ -200,7 +200,7 @@ module Gitlab .project([*order_by_columns.original_column_names_as_arel_string, Arel.sql('position')]) .from("UNNEST(#{list(order_by_columns.array_aggregated_column_names)}) WITH ORDINALITY AS u(#{list(order_by_columns.original_column_names)}, position)") - order_by_columns.each { |column| q.where(Arel.sql(column.original_column_name).not_eq(nil)) } # ignore rows where all columns are NULL + order_by_columns.each { |c| q.where(Arel.sql(c.original_column_name).not_eq(nil)) unless c.column.nullable? } # ignore rows where all columns are NULL q.order(Arel.sql(order_by_without_table_references)).take(1) end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index d182dc9f95f..403165a3935 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -59,6 +59,11 @@ FactoryBot.define do target { design } end + factory :design_updated_event, traits: [:has_design] do + action { :updated } + target { design } + end + factory :project_created_event do project factory: :project action { :created } diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb index 6019d22059d..d7f7bb9cebe 100644 --- a/spec/finders/user_recent_events_finder_spec.rb +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -8,9 +8,9 @@ RSpec.describe UserRecentEventsFinder do let_it_be(:private_project) { create(:project, :private, creator: project_owner) } let_it_be(:internal_project) { create(:project, :internal, creator: project_owner) } let_it_be(:public_project) { create(:project, :public, creator: project_owner) } - let!(:private_event) { create(:event, project: private_project, author: project_owner) } - let!(:internal_event) { create(:event, project: internal_project, author: project_owner) } - let!(:public_event) { create(:event, project: public_project, author: project_owner) } + let_it_be(:private_event) { create(:event, project: private_project, author: project_owner) } + let_it_be(:internal_event) { create(:event, project: internal_project, author: project_owner) } + let_it_be(:public_event) { create(:event, project: public_project, author: project_owner) } let_it_be(:issue) { create(:issue, project: public_project) } let(:limit) { nil } @@ -18,210 +18,266 @@ RSpec.describe UserRecentEventsFinder do subject(:finder) { described_class.new(current_user, project_owner, nil, params) } - describe '#execute' do - context 'when profile is public' do - it 'returns all the events' do - expect(finder.execute).to include(private_event, internal_event, public_event) + shared_examples 'UserRecentEventsFinder examples' do + describe '#execute' do + context 'when profile is public' do + it 'returns all the events' do + expect(finder.execute).to include(private_event, internal_event, public_event) + end end - end - context 'when profile is private' do - it 'returns no event' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false) + context 'when profile is private' do + it 'returns no event' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false) - expect(finder.execute).to be_empty + expect(finder.execute).to be_empty + end end - end - it 'does not include the events if the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false } + it 'does not include the events if the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false } - expect(finder.execute).to be_empty - end + expect(finder.execute).to be_empty + end - context 'events from multiple users' do - let_it_be(:second_user, reload: true) { create(:user) } - let_it_be(:private_project_second_user) { create(:project, :private, creator: second_user) } + context 'events from multiple users' do + let_it_be(:second_user, reload: true) { create(:user) } + let_it_be(:private_project_second_user) { create(:project, :private, creator: second_user) } - let(:internal_project_second_user) { create(:project, :internal, creator: second_user) } - let(:public_project_second_user) { create(:project, :public, creator: second_user) } - let!(:private_event_second_user) { create(:event, project: private_project_second_user, author: second_user) } - let!(:internal_event_second_user) { create(:event, project: internal_project_second_user, author: second_user) } - let!(:public_event_second_user) { create(:event, project: public_project_second_user, author: second_user) } + let_it_be(:internal_project_second_user) { create(:project, :internal, creator: second_user) } + let_it_be(:public_project_second_user) { create(:project, :public, creator: second_user) } + let_it_be(:private_event_second_user) { create(:event, project: private_project_second_user, author: second_user) } + let_it_be(:internal_event_second_user) { create(:event, project: internal_project_second_user, author: second_user) } + let_it_be(:public_event_second_user) { create(:event, project: public_project_second_user, author: second_user) } - it 'includes events from all users', :aggregate_failures do - events = described_class.new(current_user, [project_owner, second_user], nil, params).execute + it 'includes events from all users', :aggregate_failures do + events = described_class.new(current_user, [project_owner, second_user], nil, params).execute - expect(events).to include(private_event, internal_event, public_event) - expect(events).to include(private_event_second_user, internal_event_second_user, public_event_second_user) - expect(events.size).to eq(6) - end + expect(events).to include(private_event, internal_event, public_event) + expect(events).to include(private_event_second_user, internal_event_second_user, public_event_second_user) + expect(events.size).to eq(6) + end - context 'selected events' do - let!(:push_event) { create(:push_event, project: public_project, author: project_owner) } - let!(:push_event_second_user) { create(:push_event, project: public_project_second_user, author: second_user) } + context 'selected events' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:push_event1) { create(:push_event, project: public_project, author: project_owner) } + let_it_be(:push_event2) { create(:push_event, project: public_project_second_user, author: second_user) } + let_it_be(:merge_event1) { create(:event, :merged, target_type: MergeRequest.to_s, project: public_project, author: project_owner) } + let_it_be(:merge_event2) { create(:event, :merged, target_type: MergeRequest.to_s, project: public_project_second_user, author: second_user) } + let_it_be(:comment_event1) { create(:event, :commented, target_type: Note.to_s, project: public_project, author: project_owner) } + let_it_be(:comment_event2) { create(:event, :commented, target_type: DiffNote.to_s, project: public_project, author: project_owner) } + let_it_be(:comment_event3) { create(:event, :commented, target_type: DiscussionNote.to_s, project: public_project_second_user, author: second_user) } + let_it_be(:issue_event1) { create(:event, :created, project: public_project, target: issue, author: project_owner) } + let_it_be(:issue_event2) { create(:event, :updated, project: public_project, target: issue, author: project_owner) } + let_it_be(:issue_event3) { create(:event, :closed, project: public_project_second_user, target: issue, author: second_user) } + let_it_be(:wiki_event1) { create(:wiki_page_event, project: public_project, author: project_owner) } + let_it_be(:wiki_event2) { create(:wiki_page_event, project: public_project_second_user, author: second_user) } + let_it_be(:design_event1) { create(:design_event, project: public_project, author: project_owner) } + let_it_be(:design_event2) { create(:design_updated_event, project: public_project_second_user, author: second_user) } + + where(:event_filter, :ordered_expected_events) do + EventFilter.new(EventFilter::PUSH) | lazy { [push_event1, push_event2] } + EventFilter.new(EventFilter::MERGED) | lazy { [merge_event1, merge_event2] } + EventFilter.new(EventFilter::COMMENTS) | lazy { [comment_event1, comment_event2, comment_event3] } + EventFilter.new(EventFilter::TEAM) | lazy { [private_event, internal_event, public_event, private_event_second_user, internal_event_second_user, public_event_second_user] } + EventFilter.new(EventFilter::ISSUE) | lazy { [issue_event1, issue_event2, issue_event3] } + EventFilter.new(EventFilter::WIKI) | lazy { [wiki_event1, wiki_event2] } + EventFilter.new(EventFilter::DESIGNS) | lazy { [design_event1, design_event2] } + end - it 'only includes selected events (PUSH) from all users', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::PUSH) - events = described_class.new(current_user, [project_owner, second_user], event_filter, params).execute + with_them do + it 'only returns selected events from all users (id DESC)' do + events = described_class.new(current_user, [project_owner, second_user], event_filter, params).execute - expect(events).to contain_exactly(push_event, push_event_second_user) + expect(events).to eq(ordered_expected_events.reverse) + end + end end - end - it 'does not include events from users with private profile', :aggregate_failures do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, second_user).and_return(false) + it 'does not include events from users with private profile', :aggregate_failures do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, second_user).and_return(false) - events = described_class.new(current_user, [project_owner, second_user], nil, params).execute + events = described_class.new(current_user, [project_owner, second_user], nil, params).execute - expect(events).to contain_exactly(private_event, internal_event, public_event) - end + expect(events).to contain_exactly(private_event, internal_event, public_event) + end - context 'with pagination params' do - using RSpec::Parameterized::TableSyntax + context 'with pagination params' do + using RSpec::Parameterized::TableSyntax - where(:limit, :offset, :ordered_expected_events) do - nil | nil | lazy { [public_event_second_user, internal_event_second_user, private_event_second_user, public_event, internal_event, private_event] } - 2 | nil | lazy { [public_event_second_user, internal_event_second_user] } - nil | 4 | lazy { [internal_event, private_event] } - 2 | 2 | lazy { [private_event_second_user, public_event] } - end + where(:limit, :offset, :ordered_expected_events) do + nil | nil | lazy { [public_event_second_user, internal_event_second_user, private_event_second_user, public_event, internal_event, private_event] } + 2 | nil | lazy { [public_event_second_user, internal_event_second_user] } + nil | 4 | lazy { [internal_event, private_event] } + 2 | 2 | lazy { [private_event_second_user, public_event] } + end - with_them do - let(:params) { { limit: limit, offset: offset }.compact } + with_them do + let(:params) { { limit: limit, offset: offset }.compact } - it 'returns paginated events sorted by id (DESC)' do - events = described_class.new(current_user, [project_owner, second_user], nil, params).execute + it 'returns paginated events sorted by id (DESC)' do + events = described_class.new(current_user, [project_owner, second_user], nil, params).execute - expect(events).to eq(ordered_expected_events) + expect(events).to eq(ordered_expected_events) + end end end end - end - context 'filter activity events' do - let!(:push_event) { create(:push_event, project: public_project, author: project_owner) } - let!(:merge_event) { create(:event, :merged, project: public_project, author: project_owner) } - let!(:issue_event) { create(:event, :closed, project: public_project, target: issue, author: project_owner) } - let!(:comment_event) { create(:event, :commented, project: public_project, author: project_owner) } - let!(:wiki_event) { create(:wiki_page_event, project: public_project, author: project_owner) } - let!(:design_event) { create(:design_event, project: public_project, author: project_owner) } - let!(:team_event) { create(:event, :joined, project: public_project, author: project_owner) } - - it 'includes all events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::ALL) - events = described_class.new(current_user, project_owner, event_filter, params).execute - - expect(events).to include(private_event, internal_event, public_event) - expect(events).to include(push_event, merge_event, issue_event, comment_event, wiki_event, design_event, team_event) - expect(events.size).to eq(10) - end + context 'filter activity events' do + let_it_be(:push_event) { create(:push_event, project: public_project, author: project_owner) } + let_it_be(:merge_event) { create(:event, :merged, project: public_project, author: project_owner) } + let_it_be(:issue_event) { create(:event, :closed, project: public_project, target: issue, author: project_owner) } + let_it_be(:comment_event) { create(:event, :commented, project: public_project, author: project_owner) } + let_it_be(:wiki_event) { create(:wiki_page_event, project: public_project, author: project_owner) } + let_it_be(:design_event) { create(:design_event, project: public_project, author: project_owner) } + let_it_be(:team_event) { create(:event, :joined, project: public_project, author: project_owner) } + + it 'includes all events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::ALL) + events = described_class.new(current_user, project_owner, event_filter, params).execute + + expect(events).to include(private_event, internal_event, public_event) + expect(events).to include(push_event, merge_event, issue_event, comment_event, wiki_event, design_event, team_event) + expect(events.size).to eq(10) + end - it 'only includes push events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::PUSH) - events = described_class.new(current_user, project_owner, event_filter, params).execute + context 'when unknown filter is given' do + it 'includes returns all events', :aggregate_failures do + event_filter = EventFilter.new('unknown') + allow(event_filter).to receive(:filter).and_return('unknown') - expect(events).to include(push_event) - expect(events.size).to eq(1) - end + events = described_class.new(current_user, [project_owner], event_filter, params).execute - it 'only includes merge events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::MERGED) - events = described_class.new(current_user, project_owner, event_filter, params).execute + expect(events).to include(private_event, internal_event, public_event) + expect(events).to include(push_event, merge_event, issue_event, comment_event, wiki_event, design_event, team_event) + expect(events.size).to eq(10) + end + end - expect(events).to include(merge_event) - expect(events.size).to eq(1) - end + it 'only includes push events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::PUSH) + events = described_class.new(current_user, project_owner, event_filter, params).execute - it 'only includes issue events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::ISSUE) - events = described_class.new(current_user, project_owner, event_filter, params).execute + expect(events).to include(push_event) + expect(events.size).to eq(1) + end - expect(events).to include(issue_event) - expect(events.size).to eq(1) - end + it 'only includes merge events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::MERGED) + events = described_class.new(current_user, project_owner, event_filter, params).execute - it 'only includes comments events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::COMMENTS) - events = described_class.new(current_user, project_owner, event_filter, params).execute + expect(events).to include(merge_event) + expect(events.size).to eq(1) + end - expect(events).to include(comment_event) - expect(events.size).to eq(1) - end + it 'only includes issue events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::ISSUE) + events = described_class.new(current_user, project_owner, event_filter, params).execute - it 'only includes wiki events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::WIKI) - events = described_class.new(current_user, project_owner, event_filter, params).execute + expect(events).to include(issue_event) + expect(events.size).to eq(1) + end - expect(events).to include(wiki_event) - expect(events.size).to eq(1) - end + it 'only includes comments events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::COMMENTS) + events = described_class.new(current_user, project_owner, event_filter, params).execute - it 'only includes design events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::DESIGNS) - events = described_class.new(current_user, project_owner, event_filter, params).execute + expect(events).to include(comment_event) + expect(events.size).to eq(1) + end - expect(events).to include(design_event) - expect(events.size).to eq(1) - end + it 'only includes wiki events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::WIKI) + events = described_class.new(current_user, project_owner, event_filter, params).execute - it 'only includes team events', :aggregate_failures do - event_filter = EventFilter.new(EventFilter::TEAM) - events = described_class.new(current_user, project_owner, event_filter, params).execute + expect(events).to include(wiki_event) + expect(events.size).to eq(1) + end - expect(events).to include(private_event, internal_event, public_event, team_event) - expect(events.size).to eq(4) - end - end + it 'only includes design events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::DESIGNS) + events = described_class.new(current_user, project_owner, event_filter, params).execute - describe 'issue activity events' do - let(:issue) { create(:issue, project: public_project) } - let(:note) { create(:note_on_issue, noteable: issue, project: public_project) } - let!(:event_a) { create(:event, :commented, target: note, author: project_owner) } - let!(:event_b) { create(:event, :closed, target: issue, author: project_owner) } + expect(events).to include(design_event) + expect(events.size).to eq(1) + end - it 'includes all issue related events', :aggregate_failures do - events = finder.execute + it 'only includes team events', :aggregate_failures do + event_filter = EventFilter.new(EventFilter::TEAM) + events = described_class.new(current_user, project_owner, event_filter, params).execute - expect(events).to include(event_a) - expect(events).to include(event_b) + expect(events).to include(private_event, internal_event, public_event, team_event) + expect(events.size).to eq(4) + end end - end - context 'limits' do - before do - stub_const("#{described_class}::DEFAULT_LIMIT", 1) - stub_const("#{described_class}::MAX_LIMIT", 3) - end + describe 'issue activity events' do + let(:issue) { create(:issue, project: public_project) } + let(:note) { create(:note_on_issue, noteable: issue, project: public_project) } + let!(:event_a) { create(:event, :commented, target: note, author: project_owner) } + let!(:event_b) { create(:event, :closed, target: issue, author: project_owner) } - context 'when limit is not set' do - it 'returns events limited to DEFAULT_LIMIT' do - expect(finder.execute.size).to eq(described_class::DEFAULT_LIMIT) + it 'includes all issue related events', :aggregate_failures do + events = finder.execute + + expect(events).to include(event_a) + expect(events).to include(event_b) end end - context 'when limit is set' do - let(:limit) { 2 } + context 'limits' do + before do + stub_const("#{described_class}::DEFAULT_LIMIT", 1) + stub_const("#{described_class}::MAX_LIMIT", 3) + end - it 'returns events limited to specified limit' do - expect(finder.execute.size).to eq(limit) + context 'when limit is not set' do + it 'returns events limited to DEFAULT_LIMIT' do + expect(finder.execute.size).to eq(described_class::DEFAULT_LIMIT) + end end - end - context 'when limit is set to a number that exceeds maximum limit' do - let(:limit) { 4 } + context 'when limit is set' do + let(:limit) { 2 } - before do - create(:event, project: public_project, author: project_owner) + it 'returns events limited to specified limit' do + expect(finder.execute.size).to eq(limit) + end end - it 'returns events limited to MAX_LIMIT' do - expect(finder.execute.size).to eq(described_class::MAX_LIMIT) + context 'when limit is set to a number that exceeds maximum limit' do + let(:limit) { 4 } + + before do + create(:event, project: public_project, author: project_owner) + end + + it 'returns events limited to MAX_LIMIT' do + expect(finder.execute.size).to eq(described_class::MAX_LIMIT) + end end end end end + + context 'when the optimized_followed_users_queries FF is on' do + before do + stub_feature_flags(optimized_followed_users_queries: true) + end + + it_behaves_like 'UserRecentEventsFinder examples' + end + + context 'when the optimized_followed_users_queries FF is off' do + before do + stub_feature_flags(optimized_followed_users_queries: false) + end + + it_behaves_like 'UserRecentEventsFinder examples' + end end diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js index 0b36d2a940d..0761d04229c 100644 --- a/spec/frontend/environments/environment_item_spec.js +++ b/spec/frontend/environments/environment_item_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; import { cloneDeep } from 'lodash'; import { format } from 'timeago.js'; import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; @@ -44,10 +45,16 @@ describe('Environment item', () => { const findAutoStop = () => wrapper.find('.js-auto-stop'); const findUpcomingDeployment = () => wrapper.find('[data-testid="upcoming-deployment"]'); + const findLastDeployment = () => wrapper.find('[data-testid="environment-deployment-id-cell"]'); const findUpcomingDeploymentContent = () => wrapper.find('[data-testid="upcoming-deployment-content"]'); const findUpcomingDeploymentStatusLink = () => wrapper.find('[data-testid="upcoming-deployment-status-link"]'); + const findLastDeploymentAvatarLink = () => findLastDeployment().findComponent(GlAvatarLink); + const findLastDeploymentAvatar = () => findLastDeployment().findComponent(GlAvatar); + const findUpcomingDeploymentAvatarLink = () => + findUpcomingDeployment().findComponent(GlAvatarLink); + const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar); afterEach(() => { wrapper.destroy(); @@ -79,9 +86,19 @@ describe('Environment item', () => { describe('With user information', () => { it('should render user avatar with link to profile', () => { - expect(wrapper.find('.js-deploy-user-container').props('linkHref')).toEqual( - environment.last_deployment.user.web_url, - ); + const avatarLink = findLastDeploymentAvatarLink(); + const avatar = findLastDeploymentAvatar(); + const { username, avatar_url, web_url } = environment.last_deployment.user; + + expect(avatarLink.attributes('href')).toBe(web_url); + expect(avatar.props()).toMatchObject({ + src: avatar_url, + entityName: username, + }); + expect(avatar.attributes()).toMatchObject({ + title: username, + alt: `${username}'s avatar`, + }); }); }); @@ -108,9 +125,16 @@ describe('Environment item', () => { describe('When the envionment has an upcoming deployment', () => { describe('When the upcoming deployment has a deployable', () => { it('should render the build ID and user', () => { - expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText( - '#27 by upcoming-username', - ); + const avatarLink = findUpcomingDeploymentAvatarLink(); + const avatar = findUpcomingDeploymentAvatar(); + const { username, avatar_url, web_url } = environment.upcoming_deployment.user; + + expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by'); + expect(avatarLink.attributes('href')).toBe(web_url); + expect(avatar.props()).toMatchObject({ + src: avatar_url, + entityName: username, + }); }); it('should render a status icon with a link and tooltip', () => { @@ -139,10 +163,17 @@ describe('Environment item', () => { }); }); - it('should still renders the build ID and user', () => { - expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText( - '#27 by upcoming-username', - ); + it('should still render the build ID and user avatar', () => { + const avatarLink = findUpcomingDeploymentAvatarLink(); + const avatar = findUpcomingDeploymentAvatar(); + const { username, avatar_url, web_url } = environment.upcoming_deployment.user; + + expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText('#27 by'); + expect(avatarLink.attributes('href')).toBe(web_url); + expect(avatar.props()).toMatchObject({ + src: avatar_url, + entityName: username, + }); }); it('should not render the status icon', () => { @@ -383,7 +414,7 @@ describe('Environment item', () => { }); it('should hide non-folder properties', () => { - expect(wrapper.find('[data-testid="environment-deployment-id-cell"]').exists()).toBe(false); + expect(findLastDeployment().exists()).toBe(false); expect(wrapper.find('[data-testid="environment-build-cell"]').exists()).toBe(false); }); }); diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js index f9eb6dd05f3..888b49f3e0c 100644 --- a/spec/frontend/reports/components/report_section_spec.js +++ b/spec/frontend/reports/components/report_section_spec.js @@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; import reportSection from '~/reports/components/report_section.vue'; describe('Report section', () => { @@ -9,6 +10,7 @@ describe('Report section', () => { let wrapper; const ReportSection = Vue.extend(reportSection); const findCollapseButton = () => wrapper.findByTestId('report-section-expand-button'); + const findPopover = () => wrapper.findComponent(HelpPopover); const resolvedIssues = [ { @@ -269,4 +271,33 @@ describe('Report section', () => { expect(vm.$el.textContent.trim()).not.toContain('This is a success'); }); }); + + describe('help popover', () => { + describe('when popover options are defined', () => { + const options = { + title: 'foo', + content: 'bar', + }; + + beforeEach(() => { + createComponent({ + popoverOptions: options, + }); + }); + + it('popover is shown with options', () => { + expect(findPopover().props('options')).toEqual(options); + }); + }); + + describe('when popover options are not defined', () => { + beforeEach(() => { + createComponent({ popoverOptions: {} }); + }); + + it('popover is not shown', () => { + expect(findPopover().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb index acf775b3538..5c054795697 100644 --- a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb @@ -96,6 +96,12 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do expect(new_record_1.reload).to have_attributes(status: 1, original: 'updated', renamed: 'updated') expect(new_record_2.reload).to have_attributes(status: 1, original: nil, renamed: nil) end + + it 'requires the helper to run in ddl mode' do + expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!) + + migration.public_send(operation, :_test_table, :original, :renamed) + end end describe '#rename_column_concurrently' do diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index c1d2d07e956..798eee0de3e 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1390,6 +1390,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end it 'reverses the operations of cleanup_concurrent_column_type_change' do + expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!) + expect(model).to receive(:check_trigger_permissions!).with(:users) expect(model).to receive(:create_column_from).with( @@ -1415,6 +1417,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end it 'passes the type_cast_function, batch_column_name and limit' do + expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!) + expect(model).to receive(:column_exists?).with(:users, :other_batch_column).and_return(true) expect(model).to receive(:check_trigger_permissions!).with(:users) diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb index 9f4d2e04344..2ba06316507 100644 --- a/spec/lib/gitlab/metrics/rails_slis_spec.rb +++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Metrics::RailsSlis do end allow(Gitlab::RequestEndpoints).to receive(:all_api_endpoints).and_return([api_route]) - allow(Gitlab::RequestEndpoints).to receive(:all_controller_actions).and_return([[ProjectsController, 'show']]) + allow(Gitlab::RequestEndpoints).to receive(:all_controller_actions).and_return([[ProjectsController, 'index']]) allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w(foo bar))) end @@ -22,7 +22,7 @@ RSpec.describe Gitlab::Metrics::RailsSlis do request_urgency: :default }, { - endpoint_id: "ProjectsController#show", + endpoint_id: "ProjectsController#index", feature_category: :projects, request_urgency: :default } diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb index 832e269a78c..9f2ac9a953d 100644 --- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb @@ -24,12 +24,12 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder let_it_be(:issues) do [ create(:issue, project: project_1, created_at: three_weeks_ago, relative_position: 5), - create(:issue, project: project_1, created_at: two_weeks_ago), + create(:issue, project: project_1, created_at: two_weeks_ago, relative_position: nil), create(:issue, project: project_2, created_at: two_weeks_ago, relative_position: 15), - create(:issue, project: project_2, created_at: two_weeks_ago), - create(:issue, project: project_3, created_at: four_weeks_ago), + create(:issue, project: project_2, created_at: two_weeks_ago, relative_position: nil), + create(:issue, project: project_3, created_at: four_weeks_ago, relative_position: nil), create(:issue, project: project_4, created_at: five_weeks_ago, relative_position: 10), - create(:issue, project: project_5, created_at: four_weeks_ago) + create(:issue, project: project_5, created_at: four_weeks_ago, relative_position: nil) ] end @@ -155,6 +155,31 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder it_behaves_like 'correct ordering examples' end + + context 'with condition "relative_position IS NULL"' do + let(:base_scope) { Issue.where(relative_position: nil) } + let(:scope) { base_scope.order(order) } + + let(:in_operator_optimization_options) do + { + array_scope: Project.where(namespace_id: top_level_group.self_and_descendants.select(:id)).select(:id), + array_mapping_scope: -> (id_expression) { Issue.merge(base_scope.dup).where(Issue.arel_table[:project_id].eq(id_expression)) }, + finder_query: -> (_relative_position_expression, id_expression) { Issue.where(Issue.arel_table[:id].eq(id_expression)) } + } + end + + context 'when iterating records one by one' do + let(:batch_size) { 1 } + + it_behaves_like 'correct ordering examples' + end + + context 'when iterating records with LIMIT 3' do + let(:batch_size) { 3 } + + it_behaves_like 'correct ordering examples' + end + end end context 'when ordering by issues.created_at DESC, issues.id ASC' do diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index 54f1d842acc..4da19267b1c 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -59,17 +59,41 @@ RSpec.describe AwardEmoji do end end - it 'accepts custom emoji' do - user = create(:user) - group = create(:group) - group.add_maintainer(user) + context 'custom emoji' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:emoji) { create(:custom_emoji, name: 'partyparrot', namespace: group) } - project = create(:project, namespace: group) - issue = create(:issue, project: project) - emoji = create(:custom_emoji, name: 'partyparrot', namespace: group) - new_award = build(:award_emoji, user: user, awardable: issue, name: emoji.name) + before do + group.add_maintainer(user) + end + + %i[issue merge_request note_on_issue snippet].each do |awardable_type| + let_it_be(:project) { create(:project, namespace: group) } + let(:awardable) { create(awardable_type, project: project) } + + it "is accepted on #{awardable_type}" do + new_award = build(:award_emoji, user: user, awardable: awardable, name: emoji.name) - expect(new_award).to be_valid + expect(new_award).to be_valid + end + end + + it 'is accepted on subgroup issue' do + subgroup = create(:group, parent: group) + project = create(:project, namespace: subgroup) + issue = create(:issue, project: project) + new_award = build(:award_emoji, user: user, awardable: issue, name: emoji.name) + + expect(new_award).to be_valid + end + + it 'is not supported on personal snippet (yet)' do + snippet = create(:personal_snippet) + new_award = build(:award_emoji, user: snippet.author, awardable: snippet, name: 'null') + + expect(new_award).not_to be_valid + end end end @@ -223,4 +247,47 @@ RSpec.describe AwardEmoji do end end end + + describe '#url' do + let_it_be(:custom_emoji) { create(:custom_emoji) } + let_it_be(:project) { create(:project, namespace: custom_emoji.group) } + let_it_be(:issue) { create(:issue, project: project) } + + def build_award(name) + build(:award_emoji, awardable: issue, name: name) + end + + it 'is nil for built-in emoji' do + new_award = build_award('tada') + + count = ActiveRecord::QueryRecorder.new do + expect(new_award.url).to be_nil + end.count + expect(count).to be_zero + end + + it 'is nil for unrecognized emoji' do + new_award = build_award('null') + + expect(new_award.url).to be_nil + end + + it 'is set for custom emoji' do + new_award = build_award(custom_emoji.name) + + expect(new_award.url).to eq(custom_emoji.url) + end + + context 'feature flag disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it 'does not query' do + new_award = build_award(custom_emoji.name) + + expect(ActiveRecord::QueryRecorder.new { new_award.url }.count).to be_zero + end + end + end end |