Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-04-19 18:08:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-04-19 18:08:32 +0300
commit846dc476d835e43b123e0d66da3a60ed07f10641 (patch)
tree02231005811495589ab8bf4a2f4d52f80dc95117
parent2ef0b7f13d72eee215e3491a8db4623cbcdd845c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue68
-rw-r--r--app/assets/javascripts/header_search/components/app.vue6
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue6
-rw-r--r--app/assets/stylesheets/pages/search.scss30
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss27
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss23
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss5
-rw-r--r--app/controllers/autocomplete_controller.rb1
-rw-r--r--app/controllers/explore/projects_controller.rb3
-rw-r--r--app/controllers/groups/children_controller.rb3
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/controllers/users_controller.rb3
-rw-r--r--app/finders/user_recent_events_finder.rb16
-rw-r--r--app/models/award_emoji.rb6
-rw-r--r--app/models/custom_emoji.rb13
-rw-r--r--app/models/event.rb3
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/validators/gitlab/emoji_name_validator.rb6
-rw-r--r--app/views/layouts/_header_search.html.haml2
-rw-r--r--config/feature_flags/development/optimized_followed_users_queries.yml8
-rw-r--r--db/post_migrate/20220409160628_add_async_index_for_events_followed_users.rb13
-rw-r--r--db/schema_migrations/202204091606281
-rw-r--r--doc/ci/pipelines/merge_request_pipelines.md2
-rw-r--r--doc/development/pipelines.md2
-rw-r--r--doc/integration/jira/configure.md3
-rw-r--r--doc/update/upgrading_from_source.md11
-rw-r--r--doc/user/project/integrations/asana.md4
-rw-r--r--doc/user/project/integrations/bugzilla.md3
-rw-r--r--doc/user/project/integrations/discord_notifications.md5
-rw-r--r--doc/user/project/integrations/emails_on_push.md19
-rw-r--r--doc/user/project/integrations/ewm.md3
-rw-r--r--doc/user/project/integrations/irker.md5
-rw-r--r--doc/user/project/integrations/mattermost.md46
-rw-r--r--doc/user/project/integrations/pivotal_tracker.md4
-rw-r--r--doc/user/project/integrations/prometheus.md15
-rw-r--r--doc/user/project/integrations/redmine.md3
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md2
-rw-r--r--doc/user/project/integrations/unify_circuit.md3
-rw-r--r--doc/user/project/integrations/youtrack.md3
-rw-r--r--lib/api/groups.rb8
-rw-r--r--lib/api/members.rb1
-rw-r--r--lib/api/project_events.rb3
-rw-r--r--lib/api/projects.rb8
-rw-r--r--lib/api/users.rb8
-rw-r--r--lib/banzai/filter/custom_emoji_filter.rb12
-rw-r--r--lib/event_filter.rb156
-rw-r--r--lib/gitlab/database/migration_helpers.rb6
-rw-r--r--lib/gitlab/database/migration_helpers/v2.rb4
-rw-r--r--lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb4
-rw-r--r--spec/factories/events.rb5
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb356
-rw-r--r--spec/frontend/environments/environment_item_spec.js53
-rw-r--r--spec/frontend/reports/components/report_section_spec.js31
-rw-r--r--spec/lib/gitlab/database/migration_helpers/v2_spec.rb6
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb4
-rw-r--r--spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb33
-rw-r--r--spec/models/award_emoji_spec.rb85
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