Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2ef0b7f13d
commit
846dc476d8
|
@ -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"
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
13.25.0
|
||||
13.25.1
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -176,6 +176,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-not-active::after {
|
||||
color: $search-and-nav-links;
|
||||
background-color: rgba($search-and-nav-links, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
7952024a6a8df98842fa23ca9a4c328b83816ded3071e7597dbab431a5561e1a
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
||||
| --- | --- |
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
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:
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,6 +7,7 @@ module API
|
|||
before { authenticate! }
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
urgency :low
|
||||
|
||||
helpers ::API::Helpers::MembersHelpers
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not include the events if the user cannot read cross project' 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(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
|
||||
|
||||
expect(finder.execute).to be_empty
|
||||
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 }
|
||||
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) }
|
||||
|
||||
expect(finder.execute).to be_empty
|
||||
end
|
||||
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) }
|
||||
|
||||
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) }
|
||||
it 'includes events from all users', :aggregate_failures do
|
||||
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
|
||||
|
||||
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) }
|
||||
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
|
||||
|
||||
it 'includes events from all users', :aggregate_failures do
|
||||
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
|
||||
context 'selected events' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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 eq(ordered_expected_events.reverse)
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
expect(events).to eq(ordered_expected_events)
|
||||
end
|
||||
end
|
||||
end
|
||||
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 '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 'only includes selected events (PUSH) from all users', :aggregate_failures do
|
||||
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 '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')
|
||||
|
||||
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
|
||||
|
||||
it 'only includes push events', :aggregate_failures do
|
||||
event_filter = EventFilter.new(EventFilter::PUSH)
|
||||
events = described_class.new(current_user, [project_owner, second_user], event_filter, params).execute
|
||||
events = described_class.new(current_user, project_owner, event_filter, params).execute
|
||||
|
||||
expect(events).to contain_exactly(push_event, push_event_second_user)
|
||||
expect(events).to include(push_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
|
||||
|
||||
expect(events).to include(merge_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
|
||||
|
||||
expect(events).to include(issue_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
|
||||
|
||||
expect(events).to include(comment_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
|
||||
|
||||
expect(events).to include(wiki_event)
|
||||
expect(events.size).to eq(1)
|
||||
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
|
||||
|
||||
expect(events).to include(design_event)
|
||||
expect(events.size).to eq(1)
|
||||
end
|
||||
|
||||
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(private_event, internal_event, public_event, team_event)
|
||||
expect(events.size).to eq(4)
|
||||
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)
|
||||
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) }
|
||||
|
||||
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
|
||||
it 'includes all issue related events', :aggregate_failures do
|
||||
events = finder.execute
|
||||
|
||||
expect(events).to contain_exactly(private_event, internal_event, public_event)
|
||||
expect(events).to include(event_a)
|
||||
expect(events).to include(event_b)
|
||||
end
|
||||
end
|
||||
|
||||
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] }
|
||||
context 'limits' do
|
||||
before do
|
||||
stub_const("#{described_class}::DEFAULT_LIMIT", 1)
|
||||
stub_const("#{described_class}::MAX_LIMIT", 3)
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:params) { { limit: limit, offset: offset }.compact }
|
||||
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
|
||||
|
||||
it 'returns paginated events sorted by id (DESC)' do
|
||||
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
|
||||
context 'when limit is set' do
|
||||
let(:limit) { 2 }
|
||||
|
||||
expect(events).to eq(ordered_expected_events)
|
||||
it 'returns events limited to specified limit' do
|
||||
expect(finder.execute.size).to eq(limit)
|
||||
end
|
||||
end
|
||||
|
||||
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 '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
|
||||
|
||||
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
|
||||
|
||||
expect(events).to include(push_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
|
||||
|
||||
expect(events).to include(merge_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
|
||||
|
||||
expect(events).to include(issue_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
|
||||
|
||||
expect(events).to include(comment_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
|
||||
|
||||
expect(events).to include(wiki_event)
|
||||
expect(events.size).to eq(1)
|
||||
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
|
||||
|
||||
expect(events).to include(design_event)
|
||||
expect(events.size).to eq(1)
|
||||
end
|
||||
|
||||
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(private_event, internal_event, public_event, team_event)
|
||||
expect(events.size).to eq(4)
|
||||
end
|
||||
context 'when the optimized_followed_users_queries FF is on' do
|
||||
before do
|
||||
stub_feature_flags(optimized_followed_users_queries: true)
|
||||
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) }
|
||||
it_behaves_like 'UserRecentEventsFinder examples'
|
||||
end
|
||||
|
||||
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
|
||||
context 'when the optimized_followed_users_queries FF is off' do
|
||||
before do
|
||||
stub_feature_flags(optimized_followed_users_queries: false)
|
||||
end
|
||||
|
||||
context 'limits' do
|
||||
before do
|
||||
stub_const("#{described_class}::DEFAULT_LIMIT", 1)
|
||||
stub_const("#{described_class}::MAX_LIMIT", 3)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
context 'when limit is set' do
|
||||
let(:limit) { 2 }
|
||||
|
||||
it 'returns events limited to specified limit' do
|
||||
expect(finder.execute.size).to eq(limit)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
it_behaves_like 'UserRecentEventsFinder examples'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
expect(new_award).to be_valid
|
||||
%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
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue