Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-10 18:12:05 +00:00
parent 0a412bceb9
commit 2e2c1a521c
34 changed files with 581 additions and 165 deletions

View File

@ -199,6 +199,7 @@ Style/FormatString:
- 'ee/app/components/namespaces/free_user_cap/personal_usage_quota_limitations_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/preview_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb'
- 'ee/app/controllers/admin/elasticsearch_controller.rb'
- 'ee/app/controllers/admin/geo/application_controller.rb'
- 'ee/app/controllers/admin/geo/projects_controller.rb'

View File

@ -470,7 +470,7 @@ Style/IfUnlessModifier:
- 'db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb'
- 'db/post_migrate/20220131000001_schedule_trace_expiry_removal.rb'
- 'db/post_migrate/20220523171107_drop_deploy_tokens_token_column.rb'
- 'ee/app/components/namespaces/storage/limit_alert.rb'
- 'ee/app/components/namespaces/storage/limit_alert_component.rb'
- 'ee/app/controllers/admin/elasticsearch_controller.rb'
- 'ee/app/controllers/admin/emails_controller.rb'
- 'ee/app/controllers/admin/geo/application_controller.rb'

View File

@ -179,6 +179,7 @@ export default {
>
<gl-avatar
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
:entity-id="group.id"
:entity-name="group.name"
:src="group.avatarUrl"
:alt="group.name"

View File

@ -73,7 +73,9 @@ export default {
<template>
<div ref="milestoneDetails" class="issue-milestone-details">
<gl-icon :size="16" class="gl-mr-2 flex-shrink-0" name="clock" />
<span class="milestone-title d-inline-block">{{ milestone.title }}</span>
<span class="milestone-title gl-display-inline-block gl-text-truncate">{{
milestone.title
}}</span>
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
<span class="bold">{{ __('Milestone') }}</span> <br />
<span>{{ milestone.title }}</span> <br />

View File

@ -1,15 +1,25 @@
<script>
import { GlPopover, GlSkeletonLoader } from '@gitlab/ui';
import { GlIcon, GlPopover, GlSkeletonLoader, GlTooltipDirective } from '@gitlab/ui';
import query from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import StatusBox from '~/issuable/components/status_box.vue';
import { IssuableStatus } from '~/issues/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import query from '../queries/issue.query.graphql';
export default {
components: {
GlIcon,
GlPopover,
GlSkeletonLoader,
IssueDueDate,
IssueMilestone,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
StatusBox,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
target: {
@ -44,6 +54,9 @@ export default {
showDetails() {
return Object.keys(this.issue).length > 0;
},
isIssueClosed() {
return this.issue?.state === IssuableStatus.Closed;
},
},
apollo: {
issue: {
@ -69,6 +82,14 @@ export default {
</gl-skeleton-loader>
<div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
<status-box issuable-type="issue" :initial-state="issue.state" />
<gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
class="gl-text-orange-500 gl-mr-2"
:aria-label="__('Confidential')"
/>
<span class="gl-text-secondary">
{{ __('Opened') }} <time :datetime="issue.createdAt">{{ formattedTime }}</time>
</span>
@ -79,5 +100,27 @@ export default {
{{ `${projectPath}#${iid}` }}
</div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
<div v-if="!$apollo.queries.issue.loading" class="gl-display-flex gl-text-secondary gl-mt-2">
<issue-due-date
v-if="issue.dueDate"
:date="issue.dueDate.toString()"
:closed="isIssueClosed"
tooltip-placement="top"
class="gl-mr-4"
css-class="gl-display-flex gl-white-space-nowrap"
/>
<issue-weight
v-if="issue.weight"
:weight="issue.weight"
tag-name="span"
class="gl-display-flex gl-mr-4"
/>
<issue-milestone
v-if="issue.milestone"
:milestone="issue.milestone"
class="gl-display-flex gl-overflow-hidden"
/>
</div>
</gl-popover>
</template>

View File

@ -6,6 +6,14 @@ query issue($projectPath: ID!, $iid: String!) {
title
createdAt
state
confidential
dueDate
milestone {
id
title
startDate
dueDate
}
}
}
}

View File

@ -37,7 +37,7 @@ $avatar-sizes: (
),
60: (
font-size: 32px,
line-height: 58px,
line-height: 60px,
border-radius: $border-radius-large
),
64: (
@ -47,7 +47,7 @@ $avatar-sizes: (
),
90: (
font-size: 36px,
line-height: 88px,
line-height: 90px,
border-radius: $border-radius-large
),
96: (
@ -72,7 +72,6 @@ $avatar-sizes: (
float: left;
margin-right: $gl-padding;
border-radius: $avatar-radius;
border: 1px solid $t-gray-a-08;
@each $size, $size-config in $avatar-sizes {
&.s#{$size} {
@ -83,13 +82,12 @@ $avatar-sizes: (
.avatar {
transition-property: none;
width: 40px;
height: 40px;
padding: 0;
background: $gray-lightest;
overflow: hidden;
border-color: rgba($black, $gl-avatar-border-opacity);
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
&.avatar-inline {
float: none;
@ -180,6 +178,10 @@ $avatar-sizes: (
@each $size, $size-config in $avatar-sizes {
&.s#{$size} {
border-radius: map-get($size-config, 'border-radius');
.avatar {
border-radius: map-get($size-config, 'border-radius');
}
}
}
}

View File

@ -134,16 +134,6 @@
.avatar-container {
@include gl-font-weight-normal;
flex: none;
box-shadow: $avatar-box-shadow;
&.rect-avatar {
@include gl-border-none;
.avatar.s32 {
border-radius: $border-radius-default;
box-shadow: $avatar-box-shadow;
}
}
}
}

View File

@ -25,7 +25,7 @@
}
.avatar-container {
flex: 0 0 40px;
flex: 0 0 32px;
background-color: $white;
}

View File

@ -1043,7 +1043,7 @@ kbd {
text-align: left;
}
.context-header .avatar-container {
flex: 0 0 40px;
flex: 0 0 32px;
background-color: #333;
}
.context-header .sidebar-context-title {
@ -1376,18 +1376,6 @@ kbd {
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
border-style: none;
}
.nav-sidebar-inner-scroll
> div.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
margin-bottom: 60px;
@ -1400,18 +1388,6 @@ kbd {
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
border-style: none;
}
.sidebar-top-level-items
.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items
> li.active
@ -1628,7 +1604,6 @@ svg.s16 {
float: left;
margin-right: 16px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.avatar.s16,
.avatar-container.s16 {
@ -1649,7 +1624,7 @@ svg.s16 {
padding: 0;
background: #222;
overflow: hidden;
border-color: rgba(255, 255, 255, 0.1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.avatar.avatar-tile {
border-radius: 0;
@ -1714,9 +1689,15 @@ svg.s16 {
.rect-avatar.s16 {
border-radius: 2px;
}
.rect-avatar.s16 .avatar {
border-radius: 2px;
}
.rect-avatar.s32 {
border-radius: 4px;
}
.rect-avatar.s32 .avatar {
border-radius: 4px;
}
:root {
color-scheme: dark;
}
@ -1817,6 +1798,10 @@ body.gl-dark {
background-color: #262626;
border-right: 1px solid #303030;
}
.avatar-container,
.avatar {
background: rgba(255, 255, 255, 0.04);
}
.nav-sidebar li a {
color: var(--gray-600);
}

View File

@ -1022,7 +1022,7 @@ kbd {
text-align: left;
}
.context-header .avatar-container {
flex: 0 0 40px;
flex: 0 0 32px;
background-color: #fff;
}
.context-header .sidebar-context-title {
@ -1355,18 +1355,6 @@ kbd {
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
border-style: none;
}
.nav-sidebar-inner-scroll
> div.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
margin-bottom: 60px;
@ -1379,18 +1367,6 @@ kbd {
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
flex: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
border-style: none;
}
.sidebar-top-level-items
.context-header
a
.avatar-container.rect-avatar
.avatar.s32 {
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items
> li.active
@ -1607,7 +1583,6 @@ svg.s16 {
float: left;
margin-right: 16px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.avatar.s16,
.avatar-container.s16 {
@ -1628,7 +1603,7 @@ svg.s16 {
padding: 0;
background: #fdfdfd;
overflow: hidden;
border-color: rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 0 1px rgba(31, 31, 31, 0.1);
}
.avatar.avatar-tile {
border-radius: 0;
@ -1693,9 +1668,15 @@ svg.s16 {
.rect-avatar.s16 {
border-radius: 2px;
}
.rect-avatar.s16 .avatar {
border-radius: 2px;
}
.rect-avatar.s32 {
border-radius: 4px;
}
.rect-avatar.s32 .avatar {
border-radius: 4px;
}
.tab-width-8 {
tab-size: 8;

View File

@ -48,6 +48,17 @@
border-right: 1px solid $gray-50;
}
.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
background: rgba($gray-950, 0.04);
}
.gl-avatar {
@include gl-border-none;
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
}
.nav-sidebar {
li {
a {

View File

@ -4,14 +4,10 @@ module Integrations
module BaseDataFields
extend ActiveSupport::Concern
LEGACY_FOREIGN_KEY_NAME = %w(
Integrations::IssueTrackerData
).freeze
included do
# TODO: Once we rename the tables we can't rely on `table_name` anymore.
# https://gitlab.com/gitlab-org/gitlab/-/issues/331953
belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: foreign_key_name
belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :integration_id
validates :integration, presence: true
end
@ -25,16 +21,6 @@ module Integrations
algorithm: 'aes-256-gcm'
}
end
private
# Older data field models use the `service_id` foreign key for the
# integration association.
def foreign_key_name
return :service_id if self.name.in?(LEGACY_FOREIGN_KEY_NAME)
:integration_id
end
end
def activated?

View File

@ -44,7 +44,7 @@ module Integrations
end
included do
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::IssueTrackerData'
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::JiraTrackerData'
has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'

View File

@ -3,7 +3,7 @@
.context-header
= link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
%span{ class: ['avatar-container', 'settings-avatar', 's32'] }
= image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
= image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32', 'gl-rounded-full!'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
%span.sidebar-context-title= _('User Settings')
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do

View File

@ -1,8 +0,0 @@
---
name: pull_mirror_bulk_branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93211
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368797
milestone: '15.2'
type: development
group: group::source code
default_enabled: false

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RenameIssueTrackerDataServiceIdToIntegrationId < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
rename_column_concurrently :issue_tracker_data, :service_id, :integration_id
end
def down
undo_rename_column_concurrently :issue_tracker_data, :service_id, :integration_id
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class CleanupIssueTrackerDataServiceId < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :issue_tracker_data, :service_id, :integration_id
end
def down
undo_cleanup_concurrent_column_rename :issue_tracker_data, :service_id, :integration_id
end
end

View File

@ -0,0 +1 @@
4ee9f603c04284cbc0fcb6aa47ecc0f0fe238b4d68083a51f5f170edca19608b

View File

@ -0,0 +1 @@
b4ff0087acba9b91182219ea49a5a7d1bfd5b55391f0174ea62a2bfa14af03ce

View File

@ -16597,7 +16597,6 @@ ALTER SEQUENCE issue_metrics_id_seq OWNED BY issue_metrics.id;
CREATE TABLE issue_tracker_data (
id bigint NOT NULL,
service_id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
encrypted_project_url character varying,
@ -16605,7 +16604,9 @@ CREATE TABLE issue_tracker_data (
encrypted_issues_url character varying,
encrypted_issues_url_iv character varying,
encrypted_new_issue_url character varying,
encrypted_new_issue_url_iv character varying
encrypted_new_issue_url_iv character varying,
integration_id integer,
CONSTRAINT check_7ca00cd891 CHECK ((integration_id IS NOT NULL))
);
CREATE SEQUENCE issue_tracker_data_id_seq
@ -28671,7 +28672,7 @@ CREATE INDEX index_issue_metrics_on_issue_id_and_timestamps ON issue_metrics USI
CREATE INDEX index_issue_on_project_id_state_id_and_blocking_issues_count ON issues USING btree (project_id, state_id, blocking_issues_count);
CREATE INDEX index_issue_tracker_data_on_service_id ON issue_tracker_data USING btree (service_id);
CREATE INDEX index_issue_tracker_data_on_integration_id ON issue_tracker_data USING btree (integration_id);
CREATE UNIQUE INDEX index_issue_user_mentions_on_note_id ON issue_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
@ -32039,6 +32040,9 @@ ALTER TABLE ONLY approvals
ALTER TABLE ONLY namespaces
ADD CONSTRAINT fk_319256d87a FOREIGN KEY (file_template_project_id) REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE ONLY issue_tracker_data
ADD CONSTRAINT fk_33921c0ee1 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE;
ALTER TABLE ONLY namespaces
ADD CONSTRAINT fk_3448c97865 FOREIGN KEY (push_rule_id) REFERENCES push_rules(id) ON DELETE SET NULL;
@ -34055,9 +34059,6 @@ ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
ALTER TABLE ONLY operations_strategies_user_lists
ADD CONSTRAINT fk_rails_ccb7e4bc0b FOREIGN KEY (user_list_id) REFERENCES operations_user_lists(id) ON DELETE CASCADE;
ALTER TABLE ONLY issue_tracker_data
ADD CONSTRAINT fk_rails_ccc0840427 FOREIGN KEY (service_id) REFERENCES integrations(id) ON DELETE CASCADE;
ALTER TABLE ONLY resource_milestone_events
ADD CONSTRAINT fk_rails_cedf8cce4d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -1125,12 +1125,6 @@ Geo secondary sites continue to replicate and verify data, and the secondary sit
This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983).
### GitLab Pages return 404 errors after promoting
This is due to [Pages data not being managed by Geo](datatypes.md#limitations-on-replicationverification).
Find advice to resolve those error messages in the
[Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node).
### Primary site returns 500 error when accessing `/admin/geo/replication/projects`
Navigating to **Admin > Geo > Replication** (or `/admin/geo/replication/projects`) on a primary Geo site, shows a 500 error, while that same link on the secondary works fine. The primary's `production.log` has a similar entry to the following:

View File

@ -1319,25 +1319,6 @@ and in your Pages log shows this error:
sudo gitlab-ctl restart gitlab-pages
```
### 404 error after promoting a Geo secondary to a primary node
Pages files are not among the
[supported data types](../geo/replication/datatypes.md#limitations-on-replicationverification) for replication in Geo. After a secondary node is promoted to a primary node, attempts to access a Pages site result in a `404 Not Found` error.
It is possible to copy the subfolders and files in the [Pages path](#change-storage-path)
to the new primary node to resolve this.
For example, you can adapt the `rsync` strategy from the
[moving repositories documentation](../operations/moving_repositories.md).
Alternatively, run the CI pipelines of those projects that contain a `pages` job again.
### 404 or 500 error when accessing GitLab Pages in a Geo setup
Pages sites are only available on the primary Geo site, while the codebase of the project is available on all sites.
If you try to access a Pages page on a secondary site, a 404 or 500 HTTP code is returned depending on the access control.
Read more which [features don't support Geo replication/verification](../geo/replication/datatypes.md#limitations-on-replicationverification).
### Failed to connect to the internal GitLab API
If you see the following error:

View File

@ -188,6 +188,24 @@ Refer to [`strong_memoize.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/maste
end
```
Alternatively, use the `strong_memoize_attr` helper to memoize the method for you:
```ruby
class Find
include Gitlab::Utils::StrongMemoize
def result
search
end
strong_memoize_attr :result
strong_memoize_attr :enabled?, :enabled
def enabled?
Feature.enabled?(:some_feature)
end
end
```
- Clear memoization
```ruby

View File

@ -297,9 +297,6 @@ A 404 can also be related to incorrect permissions. If [Pages Access Control](pa
navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site.
To fix this, verify that the user is a member of the project.
For Geo instances, 404 errors on Pages occur after promoting a secondary to a primary.
Find more details in the [Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node)
### Cannot play media content on Safari
Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6)

View File

@ -21,6 +21,20 @@ module Gitlab
# end
# end
#
# Or like:
#
# include Gitlab::Utils::StrongMemoize
#
# def trigger_from_token
# Ci::Trigger.find_by_token(params[:token].to_s)
# end
# strong_memoize_attr :trigger_from_token
#
# strong_memoize_attr :enabled?, :enabled
# def enabled?
# Feature.enabled?(:some_feature)
# end
#
def strong_memoize(name)
key = ivar(name)
@ -40,6 +54,34 @@ module Gitlab
remove_instance_variable(key) if instance_variable_defined?(key)
end
module StrongMemoizeClassMethods
def strong_memoize_attr(method_name, member_name = nil)
member_name ||= method_name
if method_defined?(method_name) || private_method_defined?(method_name)
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
:do_strong_memoize, self, method_name, member_name)
else
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
:queue_strong_memoize, self, method_name, member_name)
end
end
def method_added(method_name)
super
if member_name = StrongMemoize
.send(:strong_memoize_queue, self).delete(method_name) # rubocop:disable GitlabSecurity/PublicSend
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
:do_strong_memoize, self, method_name, member_name)
end
end
end
def self.included(base)
base.singleton_class.prepend(StrongMemoizeClassMethods)
end
private
# Convert `"name"`/`:name` into `:@name`
@ -54,6 +96,37 @@ module Gitlab
raise ArgumentError, "Invalid type of '#{name}'"
end
end
class <<self
private
def strong_memoize_queue(klass)
klass.instance_variable_get(:@strong_memoize_queue) || klass.instance_variable_set(:@strong_memoize_queue, {})
end
def queue_strong_memoize(klass, method_name, member_name)
strong_memoize_queue(klass)[method_name] = member_name
end
def do_strong_memoize(klass, method_name, member_name)
method = klass.instance_method(method_name)
# Methods defined within a class method are already public by default, so we don't need to
# explicitly make them public.
scope = %i(private protected).find do |scope|
klass.send("#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend
.include? method_name
end
klass.define_method(method_name) do |*args, &block|
strong_memoize(member_name) do
method.bind_call(self, *args, &block)
end
end
klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end

View File

@ -106,3 +106,41 @@ RSpec.describe API::Issues, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful
end
end
RSpec.describe GraphQL::Query, type: :request do
include ApiHelpers
include GraphqlHelpers
include JavaScriptFixturesHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before_all do
project.add_reporter(user)
end
issue_popover_query_path = 'issuable/popover/queries/issue.query.graphql'
it "graphql/#{issue_popover_query_path}.json" do
query = get_graphql_query_as_string(issue_popover_query_path, ee: Gitlab.ee?)
issue = create(
:issue,
project: project,
confidential: true,
created_at: Time.parse('2020-07-01T04:08:01Z'),
due_date: Date.new(2020, 7, 5),
milestone: create(
:milestone,
project: project,
title: '15.2',
start_date: Date.new(2020, 7, 1),
due_date: Date.new(2020, 7, 30)
)
)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s })
expect_graphql_errors_to_be_empty
end
end

View File

@ -1,34 +1,21 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import issueQueryResponse from 'test_fixtures/graphql/issuable/popover/queries/issue.query.graphql.json';
import issueQuery from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import StatusBox from '~/issuable/components/status_box.vue';
import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
import issueQuery from '~/issuable/popover/queries/issue.query.graphql';
describe('Issue Popover', () => {
let wrapper;
Vue.use(VueApollo);
const issueQueryResponse = {
data: {
project: {
__typename: 'Project',
id: '1',
issue: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/1',
createdAt: '2020-07-01T04:08:01Z',
state: 'opened',
title: 'Issue title',
},
},
},
};
const mountComponent = ({
queryResponse = jest.fn().mockResolvedValue(issueQueryResponse),
} = {}) => {
@ -77,5 +64,31 @@ describe('Issue Popover', () => {
it('shows reference', () => {
expect(wrapper.text()).toContain('foo/bar#1');
});
it('shows confidential icon', () => {
const icon = wrapper.findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('eye-slash');
});
it('shows due date', () => {
const component = wrapper.findComponent(IssueDueDate);
expect(component.exists()).toBe(true);
expect(component.props('date')).toBe('2020-07-05');
expect(component.props('closed')).toBe(false);
});
it('shows milestone', () => {
const component = wrapper.findComponent(IssueMilestone);
expect(component.exists()).toBe(true);
expect(component.props('milestone')).toMatchObject({
title: '15.2',
startDate: '2020-07-01',
dueDate: '2020-07-30',
});
});
});
});

View File

@ -1,10 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require 'rspec-benchmark'
RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end
RSpec.describe Gitlab::Utils::StrongMemoize do
let(:klass) do
struct = Struct.new(:value) do
strong_memoize_class = described_class
Struct.new(:value) do
include strong_memoize_class
def self.method_added_list
@method_added_list ||= []
end
def self.method_added(name)
method_added_list << name
end
def method_name
strong_memoize(:method_name) do
trace << value
@ -12,21 +29,56 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end
end
def method_name_attr
trace << value
value
end
strong_memoize_attr :method_name_attr
strong_memoize_attr :different_method_name_attr, :different_member_name_attr
def different_method_name_attr
trace << value
value
end
strong_memoize_attr :enabled?
def enabled?
true
end
def trace
@trace ||= []
end
end
struct.include(described_class)
struct
protected
def private_method
end
private :private_method
strong_memoize_attr :private_method
public
def protected_method
end
protected :protected_method
strong_memoize_attr :protected_method
private
def public_method
end
public :public_method
strong_memoize_attr :public_method
end
end
subject(:object) { klass.new(value) }
shared_examples 'caching the value' do
it 'only calls the block once' do
value0 = object.method_name
value1 = object.method_name
value0 = object.send(method_name)
value1 = object.send(method_name)
expect(value0).to eq(value)
expect(value1).to eq(value)
@ -34,8 +86,8 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end
it 'returns and defines the instance variable for the exact value' do
returned_value = object.method_name
memoized_value = object.instance_variable_get(:@method_name)
returned_value = object.send(method_name)
memoized_value = object.instance_variable_get(:"@#{member_name}")
expect(returned_value).to eql(value)
expect(memoized_value).to eql(value)
@ -46,12 +98,19 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
[nil, false, true, 'value', 0, [0]].each do |value|
context "with value #{value}" do
let(:value) { value }
let(:method_name) { :method_name }
let(:member_name) { :method_name }
it_behaves_like 'caching the value'
it 'raises exception for invalid key' do
it 'raises exception for invalid type as key' do
expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/
end
it 'raises exception for invalid characters in key' do
expect { object.strong_memoize(:enabled?) { 20 } }
.to raise_error /is not allowed as an instance variable name/
end
end
end
@ -109,4 +168,64 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
expect(object.instance_variable_defined?(:@method_name)).to be(false)
end
end
describe '.strong_memoize_attr' do
[nil, false, true, 'value', 0, [0]].each do |value|
let(:value) { value }
context "memoized after method definition with value #{value}" do
let(:method_name) { :method_name_attr }
let(:member_name) { :method_name_attr }
it_behaves_like 'caching the value'
it 'calls the existing .method_added' do
expect(klass.method_added_list).to include(:method_name_attr)
end
end
context "memoized before method definition with different member name and value #{value}" do
let(:method_name) { :different_method_name_attr }
let(:member_name) { :different_member_name_attr }
it_behaves_like 'caching the value'
it 'calls the existing .method_added' do
expect(klass.method_added_list).to include(:different_method_name_attr)
end
end
context 'with valid method name' do
let(:method_name) { :enabled? }
context 'with invalid member name' do
let(:member_name) { :enabled? }
it 'is invalid' do
expect { object.send(method_name) { value } }.to raise_error /is not allowed as an instance variable name/
end
end
end
end
describe 'method visibility' do
it 'sets private visibility' do
expect(klass.private_instance_methods).to include(:private_method)
expect(klass.protected_instance_methods).not_to include(:private_method)
expect(klass.public_instance_methods).not_to include(:private_method)
end
it 'sets protected visibility' do
expect(klass.private_instance_methods).not_to include(:protected_method)
expect(klass.protected_instance_methods).to include(:protected_method)
expect(klass.public_instance_methods).not_to include(:protected_method)
end
it 'sets public visibility' do
expect(klass.private_instance_methods).not_to include(:public_method)
expect(klass.protected_instance_methods).not_to include(:public_method)
expect(klass.public_instance_methods).to include(:public_method)
end
end
end
end

View File

@ -11,7 +11,7 @@ RSpec.describe Integration do
describe "Associations" do
it { is_expected.to belong_to(:project).inverse_of(:integrations) }
it { is_expected.to belong_to(:group).inverse_of(:integrations) }
it { is_expected.to have_one(:issue_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:service_id).class_name('Integrations::IssueTrackerData') }
it { is_expected.to have_one(:issue_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::IssueTrackerData') }
it { is_expected.to have_one(:jira_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::JiraTrackerData') }
end

View File

@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe IssuePolicy do
include_context 'ProjectPolicyTable context'
include ExternalAuthorizationServiceHelpers
include ProjectHelpers
let(:guest) { create(:user) }
let(:author) { create(:user) }
@ -50,6 +52,19 @@ RSpec.describe IssuePolicy do
end
end
shared_examples 'grants the expected permissions' do |policy|
specify do
enable_admin_mode!(user) if admin_mode
update_feature_access_level(project, feature_access_level)
if expected_count == 1
expect(permissions(user, issue)).to be_allowed(policy)
else
expect(permissions(user, issue)).to be_disallowed(policy)
end
end
end
context 'a private project' do
let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
@ -85,7 +100,6 @@ RSpec.describe IssuePolicy do
it 'allows reporters from group links to read, update, and admin issues' do
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
end
@ -217,7 +231,7 @@ RSpec.describe IssuePolicy do
it 'allows reporters from group links to read, update, reopen and admin issues' do
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:reopen_issue)
expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
@ -454,7 +468,7 @@ RSpec.describe IssuePolicy do
end
end
context 'when peronsal namespace' do
context 'when personal namespace' do
let(:project) { create(:project) }
it 'is disallowed' do
@ -465,4 +479,34 @@ RSpec.describe IssuePolicy do
end
end
end
context 'when user is an inherited member from the group' do
let(:user) { create_user_from_membership(group, membership) }
let(:project) { create(:project, project_level, group: group) }
let(:issue) { create(:issue, project: project) }
context 'and policy allows guest access' do
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
permission_table_for_guest_feature_access
end
with_them do
it_behaves_like 'grants the expected permissions', :read_issue
it_behaves_like 'grants the expected permissions', :read_issue_iid
end
end
context 'and policy allows reporter access' do
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
permission_table_for_reporter_issue_access
end
with_them do
it_behaves_like 'grants the expected permissions', :update_issue
it_behaves_like 'grants the expected permissions', :admin_issue
it_behaves_like 'grants the expected permissions', :set_issue_metadata
it_behaves_like 'grants the expected permissions', :set_confidentiality
end
end
end
end

View File

@ -9,6 +9,8 @@ RSpec.describe API::Issues do
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
end
let_it_be(:group) { create(:group, :public) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
@ -85,6 +87,8 @@ RSpec.describe API::Issues do
end
before_all do
group.add_reporter(user)
group.add_guest(guest)
project.add_reporter(user)
project.add_guest(guest)
private_mrs_project.add_reporter(user)
@ -107,6 +111,22 @@ RSpec.describe API::Issues do
end
end
shared_examples 'returns project issues without confidential issues for guests' do
specify do
get api(api_url, guest)
expect_paginated_array_response_contain_exactly(open_issue.id, closed_issue.id)
end
end
shared_examples 'returns all project issues for reporters' do
specify do
get api(api_url, user)
expect_paginated_array_response_contain_exactly(open_issue.id, confidential_issue.id, closed_issue.id)
end
end
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
@ -183,6 +203,30 @@ RSpec.describe API::Issues do
end
end
context 'when user is an inherited member from the group' do
let!(:open_issue) { create(:issue, project: group_project) }
let!(:confidential_issue) { create(:issue, :confidential, project: group_project) }
let!(:closed_issue) { create(:issue, state: :closed, project: group_project) }
let!(:api_url) { "/projects/#{group_project.id}/issues" }
context 'and group project is public and issues are private' do
let_it_be(:group_project) do
create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group)
end
it_behaves_like 'returns project issues without confidential issues for guests'
it_behaves_like 'returns all project issues for reporters'
end
context 'and group project is private' do
let_it_be(:group_project) { create(:project, :private, group: group) }
it_behaves_like 'returns project issues without confidential issues for guests'
it_behaves_like 'returns all project issues for reporters'
end
end
it 'avoids N+1 queries' do
get api("/projects/#{project.id}/issues", user)

View File

@ -66,6 +66,13 @@ module ApiHelpers
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
end
def expect_paginated_array_response_contain_exactly(*items)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
end
def stub_last_activity_update
allow_any_instance_of(Users::ActivityService).to receive(:execute)
end

View File

@ -545,5 +545,62 @@ RSpec.shared_context 'ProjectPolicyTable context' do
:private | :non_member | nil | 0
:private | :anonymous | nil | 0
end
# Based on the permission_table_for_reporter_feature_access table, but for issue
# features where public and internal projects with issues enabled only allow
# access to reporters and above (excluding admins if admin mode is disabled)
#
# project_level, :feature_access_level, :membership, :admin_mode, :expected_count
def permission_table_for_reporter_issue_access
:public | :enabled | :admin | true | 1
:public | :enabled | :admin | false | 0
:public | :enabled | :reporter | nil | 1
:public | :enabled | :guest | nil | 0
:public | :enabled | :non_member | nil | 0
:public | :enabled | :anonymous | nil | 0
:public | :private | :admin | true | 1
:public | :private | :admin | false | 0
:public | :private | :reporter | nil | 1
:public | :private | :guest | nil | 0
:public | :private | :non_member | nil | 0
:public | :private | :anonymous | nil | 0
:public | :disabled | :reporter | nil | 0
:public | :disabled | :guest | nil | 0
:public | :disabled | :non_member | nil | 0
:public | :disabled | :anonymous | nil | 0
:internal | :enabled | :admin | true | 1
:internal | :enabled | :admin | false | 0
:internal | :enabled | :reporter | nil | 1
:internal | :enabled | :guest | nil | 0
:internal | :enabled | :non_member | nil | 0
:internal | :enabled | :anonymous | nil | 0
:internal | :private | :admin | true | 1
:internal | :private | :admin | false | 0
:internal | :private | :reporter | nil | 1
:internal | :private | :guest | nil | 0
:internal | :private | :non_member | nil | 0
:internal | :private | :anonymous | nil | 0
:internal | :disabled | :reporter | nil | 0
:internal | :disabled | :guest | nil | 0
:internal | :disabled | :non_member | nil | 0
:internal | :disabled | :anonymous | nil | 0
:private | :private | :admin | true | 1
:private | :private | :admin | false | 0
:private | :private | :reporter | nil | 1
:private | :private | :guest | nil | 0
:private | :private | :non_member | nil | 0
:private | :private | :anonymous | nil | 0
:private | :disabled | :reporter | nil | 0
:private | :disabled | :guest | nil | 0
:private | :disabled | :non_member | nil | 0
:private | :disabled | :anonymous | nil | 0
end
# rubocop:enable Metrics/AbcSize
end