Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-12-01 03:13:34 +00:00
parent 08775893a8
commit a3e0d4c59f
49 changed files with 544 additions and 95 deletions

View File

@ -197,7 +197,7 @@ gem 'seed-fu', '~> 2.3.7' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-model', '~> 7.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-api', '7.13.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-core', '~> 3.189.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-core', '~> 3.190.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-s3', '~> 1.141.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -37,7 +37,7 @@
{"name":"aws-eventstream","version":"1.3.0","platform":"ruby","checksum":"f1434cc03ab2248756eb02cfa45e900e59a061d7fbdc4a9fd82a5dd23d796d3f"},
{"name":"aws-partitions","version":"1.761.0","platform":"ruby","checksum":"291e444e1edfc92c5521a6dbdd1236ccc3f122b3520163b2be6ec5b6ef350ef2"},
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
{"name":"aws-sdk-core","version":"3.189.0","platform":"ruby","checksum":"a2b5d55c2f3827c8e453a228b011ac5c0074a09c301e289774151225114c7ecf"},
{"name":"aws-sdk-core","version":"3.190.0","platform":"ruby","checksum":"a3455fb3fc1691dd5331282ff16cb0b2ef136a5b63ed68b77e9fda447ea7cfa6"},
{"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"},
{"name":"aws-sdk-s3","version":"1.141.0","platform":"ruby","checksum":"cadb88497af6736e86a4a1fc8eb42333fb27ae85901686334252c50862bdd02e"},
{"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"},

View File

@ -291,7 +291,7 @@ GEM
aws-sdk-cloudformation (1.41.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sdk-core (3.189.0)
aws-sdk-core (3.190.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
@ -1792,7 +1792,7 @@ DEPENDENCIES
autoprefixer-rails (= 10.2.5.1)
awesome_print
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.189.0)
aws-sdk-core (~> 3.190.0)
aws-sdk-s3 (~> 1.141.0)
axe-core-rspec
babosa (~> 2.0)

View File

@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
showCommentForm: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -60,6 +65,16 @@ export default {
: '';
},
},
watch: {
showCommentForm: {
immediate: true,
handler(focus) {
if (focus) {
this.isEditing = true;
}
},
},
},
methods: {
async addNote({ commentText }) {
this.isSubmitting = true;

View File

@ -28,6 +28,7 @@ export default {
data() {
return {
isExpanded: true,
showCommentForm: false,
};
},
computed: {
@ -54,16 +55,24 @@ export default {
toggleDiscussion() {
this.isExpanded = !this.isExpanded;
},
startReplying() {
this.showCommentForm = true;
},
stopReplying() {
this.showCommentForm = false;
},
},
};
</script>
<template>
<abuse-report-note
v-if="!hasReplies"
v-if="!hasReplies && !showCommentForm"
:note="note"
:abuse-report-id="abuseReportId"
show-reply-button
class="gl-mb-4"
@startReplying="startReplying"
/>
<timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
<div class="timeline-content">
@ -76,7 +85,9 @@ export default {
:note="note"
:discussion-id="discussionId"
:abuse-report-id="abuseReportId"
show-reply-button
class="gl-mb-4"
@startReplying="startReplying"
/>
<discussion-notes-replies-wrapper>
<toggle-replies-widget
@ -97,7 +108,9 @@ export default {
<abuse-report-add-note
:discussion-id="discussionId"
:is-new-discussion="false"
:show-comment-form="showCommentForm"
:abuse-report-id="abuseReportId"
@cancelEditing="stopReplying"
/>
</template>
</discussion-notes-replies-wrapper>

View File

@ -5,6 +5,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteBody from './abuse_report_note_body.vue';
import AbuseReportNoteActions from './abuse_report_note_actions.vue';
export default {
name: 'AbuseReportNote',
@ -17,6 +18,7 @@ export default {
TimelineEntryItem,
NoteHeader,
NoteBody,
AbuseReportNoteActions,
},
props: {
abuseReportId: {
@ -27,6 +29,11 @@ export default {
type: Object,
required: true,
},
showReplyButton: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
noteAnchorId() {
@ -39,6 +46,11 @@ export default {
return getIdFromGraphQLId(this.author.id);
},
},
methods: {
startReplying() {
this.$emit('startReplying');
},
},
};
</script>
@ -70,6 +82,12 @@ export default {
>
<span v-if="note.createdAt" class="d-none d-sm-inline">&middot;</span>
</note-header>
<div class="gl-display-inline-flex">
<abuse-report-note-actions
:show-reply-button="showReplyButton"
@startReplying="startReplying"
/>
</div>
</div>
<div class="timeline-discussion-body">

View File

@ -0,0 +1,27 @@
<script>
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
export default {
name: 'AbuseReportNoteActions',
components: {
ReplyButton,
},
props: {
showReplyButton: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="note-actions">
<reply-button
v-if="showReplyButton"
ref="replyButton"
@startReplying="$emit('startReplying')"
/>
</div>
</template>

View File

@ -23,6 +23,7 @@ export async function mountIssuesDashboardApp() {
emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueDateFilterFeature,
hasIssueWeightsFeature,
hasScopedLabelsFeature,
initialSort,
@ -47,6 +48,7 @@ export async function mountIssuesDashboardApp() {
emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueDateFilterFeature: parseBoolean(hasIssueDateFilterFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,

View File

@ -130,9 +130,7 @@ class IssuableFinder
end
def filter_items(items)
# Selection by group is already covered by `by_project` and `projects` for project-based issuables
# Group-based issuables have their own group filter methods
items = by_project(items)
items = by_parent(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
@ -313,7 +311,7 @@ class IssuableFinder
end
# rubocop: disable CodeReuse/ActiveRecord
def by_project(items)
def by_parent(items)
# When finding issues for multiple projects it's more efficient
# to use a JOIN instead of running a sub-query
# See https://gitlab.com/gitlab-org/gitlab/-/commit/8591cc02be6b12ed60f763a5e0147f2cbbca99e1

View File

@ -109,6 +109,21 @@ class IssuesFinder < IssuableFinder
super.with_projects_matching_search_data
end
override :by_parent
def by_parent(items)
return super unless include_namespace_level_work_items?
items.in_namespaces(
Namespace.from_union(
[
Group.id_in(params.group).select(:id),
params.projects.select(:project_namespace_id)
],
remove_duplicates: false
)
)
end
def by_confidential(items)
return items if params[:confidential].nil?
@ -157,6 +172,12 @@ class IssuesFinder < IssuableFinder
def model_class
Issue
end
def include_namespace_level_work_items?
params.group? &&
Array(params[:issue_types]).map(&:to_s).include?('epic') &&
Feature.enabled?(:namespace_level_work_items, params.group)
end
end
IssuesFinder.prepend_mod_with('IssuesFinder')

View File

@ -54,8 +54,8 @@ module WorkItems
end
strong_memoize_attr :namespace
override :by_project
def by_project(items)
override :by_parent
def by_parent(items)
items
end
end

View File

@ -10,7 +10,7 @@ module Resolvers
include GroupIssuableResolver
before_connection_authorization do |nodes, _|
projects = nodes.map(&:project)
projects = nodes.filter_map(&:project)
ActiveRecord::Associations::Preloader.new(records: projects, associations: project_associations).call
end

View File

@ -91,13 +91,13 @@ module Types
description: 'Web URL of the issue.'
field :emails_disabled, GraphQL::Types::Boolean, null: false,
method: :project_emails_disabled?,
description: 'Indicates if a project has email notifications disabled: `true` if email notifications are disabled.',
method: :parent_emails_disabled?,
description: 'Indicates if the parent project or group has email notifications disabled: `true` if email notifications are disabled.',
deprecated: { reason: 'Use `emails_enabled`', milestone: '16.3' }
field :emails_enabled, GraphQL::Types::Boolean, null: false,
method: :project_emails_enabled?,
description: 'Indicates if a project has email notifications disabled: `false` if email notifications are disabled.'
method: :parent_emails_enabled?,
description: 'Indicates if the parent project or group has email notifications disabled: `false` if email notifications are disabled.'
field :human_time_estimate, GraphQL::Types::String, null: true,
description: 'Human-readable time estimate of the issue.'
@ -162,7 +162,7 @@ module Types
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the issue.'
field :project_id, GraphQL::Types::Int, null: false, method: :project_id,
field :project_id, GraphQL::Types::Int, null: true, method: :project_id,
description: 'ID of the issue project.'
field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true,

View File

@ -20,5 +20,9 @@ module Types
value 'KEY_RESULT', value: 'key_result',
description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.7' }
value 'EPIC', value: 'epic',
description: 'Epic issue type. ' \
'Available only when feature flag `namespace_level_work_items` is enabled.',
alpha: { milestone: '16.7' }
end
end

View File

@ -166,7 +166,7 @@ module IssuesHelper
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: url_for(safe_params.merge(rss_url_options)),
sign_in_path: new_user_session_path,
has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, namespace).to_s
has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, current_user).to_s
}
end
@ -218,6 +218,7 @@ module IssuesHelper
dashboard_milestones_path: dashboard_milestones_path(format: :json),
empty_state_with_filter_svg_path: image_path('illustrations/empty-state/empty-issues-md.svg'),
empty_state_without_filter_svg_path: image_path('illustrations/issue-dashboard_results-without-filter.svg'),
has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, current_user).to_s,
initial_sort: current_user&.user_preference&.issues_sort,
is_public_visibility_restricted:
Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,

View File

@ -132,16 +132,16 @@ class Issue < ApplicationRecord
validate :due_date_after_start_date
validate :parent_link_confidentiality
alias_method :issuing_parent, :project
alias_attribute :issuing_parent_id, :project_id
alias_attribute :external_author, :service_desk_reply_to
pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
scope :in_namespaces, ->(namespaces) { where(namespace: namespaces) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :not_in_projects, ->(project_ids) { where.not(project_id: project_ids) }
scope :non_archived, -> { left_joins(:project).where(project_id: nil).or(where(projects: { archived: false })) }
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
@ -732,6 +732,11 @@ class Issue < ApplicationRecord
def resource_parent
project || namespace
end
alias_method :issuing_parent, :resource_parent
def issuing_parent_id
project_id.presence || namespace_id
end
# Persisted records will always have a work_item_type. This method is useful
# in places where we use a non persisted issue to perform feature checks

View File

@ -35,10 +35,13 @@ module Users
scope :for_user, -> (user_id) { where(user_id: user_id) }
def self.related_to_banned_user?(international_dial_code, phone_number)
joins(:banned_user).where(
joins(:banned_user)
.where(
international_dial_code: international_dial_code,
phone_number: phone_number
).exists?
)
.where.not(validated_at: nil)
.exists?
end
def self.by_reference_id(ref_id)

View File

@ -23,7 +23,6 @@ class WorkItem < Issue
foreign_key: :work_item_id, source: :work_item
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
scope :in_namespaces, ->(namespaces) { where(namespace: namespaces) }
scope :with_confidentiality_check, ->(user) {
confidential_query = <<~SQL

View File

@ -12,12 +12,12 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
issue.subscribed?(current_user, issue.project)
end
def project_emails_disabled?
issue.project.emails_disabled?
def parent_emails_disabled?
issue.resource_parent.emails_disabled?
end
def project_emails_enabled?
issue.project.emails_enabled?
def parent_emails_enabled?
issue.resource_parent.emails_enabled?
end
delegator_override :service_desk_reply_to

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class DropProjectsOnPathAndIdIndex < Gitlab::Database::Migration[2.2]
milestone '16.7'
disable_ddl_transaction!
TABLE_NAME = :projects
INDEX_NAME = :index_projects_on_path_and_id
def up
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
end
def down
add_concurrent_index TABLE_NAME, [:path, :id], name: INDEX_NAME
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class DropProjectsOnCreatedAtAndIdIndex < Gitlab::Database::Migration[2.2]
milestone '16.7'
disable_ddl_transaction!
TABLE_NAME = :projects
INDEX_NAME = :index_projects_on_created_at_and_id
def up
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
end
def down
add_concurrent_index TABLE_NAME, [:created_at, :id], name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
4a5bf054f8bea3ec51060cc4cd3a18f12fb40e13edb8a5a8d99f9d25e631dd30

View File

@ -0,0 +1 @@
4267ce10078606ae7829e5b1afd27e64c7e15603d87dd0c1a52a683ae8fb9e28

View File

@ -34158,8 +34158,6 @@ CREATE INDEX index_projects_id_for_aimed_for_deletion ON projects USING btree (i
CREATE INDEX index_projects_not_aimed_for_deletion ON projects USING btree (id) WHERE (marked_for_deletion_at IS NULL);
CREATE INDEX index_projects_on_created_at_and_id ON projects USING btree (created_at, id);
CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON projects USING btree (creator_id, created_at, id);
CREATE INDEX index_projects_on_creator_id_and_id ON projects USING btree (creator_id, id);
@ -34202,8 +34200,6 @@ CREATE INDEX index_projects_on_namespace_id_and_repository_size_limit ON project
CREATE INDEX index_projects_on_organization_id ON projects USING btree (organization_id);
CREATE INDEX index_projects_on_path_and_id ON projects USING btree (path, id);
CREATE INDEX index_projects_on_path_trigram ON projects USING gin (path gin_trgm_ops);
CREATE INDEX index_projects_on_pending_delete ON projects USING btree (pending_delete);

View File

@ -18123,7 +18123,7 @@ Relationship between an epic and an issue.
| <a id="epicissuedownvotes"></a>`downvotes` | [`Int!`](#int) | Number of downvotes the issue has received. |
| <a id="epicissueduedate"></a>`dueDate` | [`Time`](#time) | Due date of the issue. |
| <a id="epicissueemailsdisabled"></a>`emailsDisabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.3. Use `emails_enabled`. |
| <a id="epicissueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `false` if email notifications are disabled. |
| <a id="epicissueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if the parent project or group has email notifications disabled: `false` if email notifications are disabled. |
| <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. |
| <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
@ -18145,7 +18145,7 @@ Relationship between an epic and an issue.
| <a id="epicissuemoved"></a>`moved` | [`Boolean`](#boolean) | Indicates if issue got moved from other project. |
| <a id="epicissuemovedto"></a>`movedTo` | [`Issue`](#issue) | Updated Issue after it got moved to another project. |
| <a id="epicissueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
| <a id="epicissueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
| <a id="epicissueprojectid"></a>`projectId` | [`Int`](#int) | ID of the issue project. |
| <a id="epicissuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) |
| <a id="epicissuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) |
| <a id="epicissuerelationpath"></a>`relationPath` | [`String`](#string) | URI path of the epic-issue relation. |
@ -20524,7 +20524,7 @@ Describes an issuable resource link for incident issues.
| <a id="issuedownvotes"></a>`downvotes` | [`Int!`](#int) | Number of downvotes the issue has received. |
| <a id="issueduedate"></a>`dueDate` | [`Time`](#time) | Due date of the issue. |
| <a id="issueemailsdisabled"></a>`emailsDisabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.3. Use `emails_enabled`. |
| <a id="issueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `false` if email notifications are disabled. |
| <a id="issueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if the parent project or group has email notifications disabled: `false` if email notifications are disabled. |
| <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
| <a id="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
@ -20545,7 +20545,7 @@ Describes an issuable resource link for incident issues.
| <a id="issuemoved"></a>`moved` | [`Boolean`](#boolean) | Indicates if issue got moved from other project. |
| <a id="issuemovedto"></a>`movedTo` | [`Issue`](#issue) | Updated Issue after it got moved to another project. |
| <a id="issueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
| <a id="issueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
| <a id="issueprojectid"></a>`projectId` | [`Int`](#int) | ID of the issue project. |
| <a id="issuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) |
| <a id="issuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) |
| <a id="issuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). |
@ -30050,6 +30050,7 @@ Issue type.
| Value | Description |
| ----- | ----------- |
| <a id="issuetypeepic"></a>`EPIC` **{warning-solid}** | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Epic issue type. Available only when feature flag `namespace_level_work_items` is enabled. |
| <a id="issuetypeincident"></a>`INCIDENT` | Incident issue type. |
| <a id="issuetypeissue"></a>`ISSUE` | Issue issue type. |
| <a id="issuetypekey_result"></a>`KEY_RESULT` **{warning-solid}** | **Introduced** in 15.7. This feature is an Experiment. It can be changed or removed at any time. Key Result issue type. Available only when feature flag `okrs_mvc` is enabled. |

View File

@ -65,9 +65,9 @@ full report available in the **Pipeline** details view.
> - [Inline annotation added](https://gitlab.com/gitlab-org/gitlab/-/issues/2526) and [feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284140) in GitLab 14.1.
Code Quality results display in the merge request **Changes** view. Lines containing Code Quality
issues are marked by an indicator beside the gutter. Hover over the marker for details of the issue.
issues are marked by a symbol beside the gutter. Select the symbol to see the list of issues, then select an issue to see its details.
![Code Quality MR diff report](img//code_quality_mr_diff_report_v15_7.png)
![Code Quality Inline Indicator](img/code_quality_inline_indicator_v16_7.png)
### Pipeline details view **(PREMIUM ALL)**

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -7,20 +7,7 @@ type: howto
# Undo options in Git **(FREE ALL)**
[Nothing in Git is deleted](https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery),
so when you work in Git, you can undo your work.
All version control systems have options for undoing work. However,
because of the de-centralized nature of Git, these options are multiplied.
The actions you take are based on the
[stage of development](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository)
you are in.
For more information about working with Git and GitLab:
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Learn why [North Western Mutual chose GitLab](https://youtu.be/kPNMyxKRRoM) for their enterprise source code management.
- Learn how to [get started with Git](https://about.gitlab.com/resources/whitepaper-moving-to-git/).
- For more advanced examples, refer to the [Git book](https://git-scm.com/book/en/v2).
Git provides options for undoing changes. The method for undoing a change depends on whether the change is unstaged, staged, committed, or pushed.
## When you can undo changes

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -17,7 +17,6 @@ any GitLab tier. The analyzers output JSON-formatted reports as job artifacts.
With GitLab Ultimate, SAST results are also processed so you can:
- See them in merge requests.
- Use them in approval workflows.
- Review them in the security dashboard.
@ -222,6 +221,7 @@ as shown in the following table:
| [Customize SAST settings](#available-cicd-variables) | **{check-circle}** | **{check-circle}** |
| Download [JSON Report](#reports-json-format) | **{check-circle}** | **{check-circle}** |
| See new findings in merge request widget | **{dotted-circle}** | **{check-circle}** |
| See new findings in merge request changes | **{dotted-circle}** | **{check-circle}** |
| [Manage vulnerabilities](../vulnerabilities/index.md) | **{dotted-circle}** | **{check-circle}** |
| [Access the Security Dashboard](../security_dashboard/index.md) | **{dotted-circle}** | **{check-circle}** |
| [Configure SAST by using the UI](#configure-sast-by-using-the-ui) | **{dotted-circle}** | **{check-circle}** |
@ -229,6 +229,35 @@ as shown in the following table:
| [Detect False Positives](#false-positive-detection) | **{dotted-circle}** | **{check-circle}** |
| [Track moved vulnerabilities](#advanced-vulnerability-tracking) | **{dotted-circle}** | **{check-circle}** |
## View SAST results
SAST results are shown in the:
- Merge request widget
- Merge request changes view
- Vulnerability Report
### Merge request widget **(ULTIMATE ALL)**
SAST results display in the merge request widget area if a report from the target
branch is available for comparison. The merge request widget displays SAST findings and resolutions that
were introduced by the changes made in the merge request.
![Security Merge request widget](img/sast_mr_widget_v16_7.png)
### Merge request changes view **(ULTIMATE ALL)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10959) in GitLab 16.6 with a [flag](../../../administration/feature_flags.md) named `sast_reports_in_inline_diff`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `sast_reports_in_inline_diff`.
On GitLab.com, this feature is not available.
SAST results display in the merge request **Changes** view. Lines containing SAST
issues are marked by a symbol beside the gutter. Select the symbol to see the list of issues, then select an issue to see its details.
![SAST Inline Indicator](img/sast_inline_indicator_v16_7.png)
## Contribute your scanner
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab.

View File

@ -43191,6 +43191,9 @@ msgstr ""
msgid "SecurityOrchestration|%{fileName} loading failed. Please try again."
msgstr ""
msgid "SecurityOrchestration|%{frameworkName} has %{projectLength} %{projects}"
msgstr ""
msgid "SecurityOrchestration|%{licenses} and %{lastLicense}"
msgstr ""
@ -43293,6 +43296,9 @@ msgstr ""
msgid "SecurityOrchestration|Compliance Framework ID(s) can only be set for group policies"
msgstr ""
msgid "SecurityOrchestration|Compliance framework has no projects"
msgstr ""
msgid "SecurityOrchestration|Create more robust vulnerability rules and apply them to all your projects."
msgstr ""

View File

@ -19,6 +19,7 @@ describe('Abuse Report Add Note', () => {
let wrapper;
const mockAbuseReportId = mockAbuseReport.report.globalId;
const mockDiscussionId = 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a';
const mutationSuccessHandler = jest.fn().mockResolvedValue(createAbuseReportNoteResponse);
@ -35,6 +36,7 @@ describe('Abuse Report Add Note', () => {
abuseReportId = mockAbuseReportId,
discussionId = '',
isNewDiscussion = true,
showCommentForm = false,
} = {}) => {
wrapper = shallowMountExtended(AbuseReportAddNote, {
apolloProvider: createMockApollo([[createNoteMutation, mutationHandler]]),
@ -42,6 +44,7 @@ describe('Abuse Report Add Note', () => {
abuseReportId,
discussionId,
isNewDiscussion,
showCommentForm,
},
});
};
@ -194,15 +197,30 @@ describe('Abuse Report Add Note', () => {
describe('Replying to a comment', () => {
beforeEach(() => {
createComponent({
discussionId: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
discussionId: mockDiscussionId,
isNewDiscussion: false,
showCommentForm: false,
});
});
it('should not show the comment form', () => {
expect(findAbuseReportCommentForm().exists()).toBe(false);
});
it('should show comment form when reply textarea is clicked on', async () => {
await findReplyTextarea().trigger('click');
expect(findAbuseReportCommentForm().exists()).toBe(true);
});
it('should show comment form if `showCommentForm` is true', () => {
createComponent({
discussionId: mockDiscussionId,
isNewDiscussion: false,
showCommentForm: true,
});
expect(findAbuseReportCommentForm().exists()).toBe(true);
});
});
});

View File

@ -80,7 +80,7 @@ describe('Abuse Report Comment Form', () => {
expect(findMarkdownEditor().props('value')).toBe('draft comment');
});
it('should pass an empty string if both draft & initialValue are empty', () => {
it('should pass an empty string if both draft and initialValue are empty', () => {
jest.spyOn(autosave, 'getDraft').mockImplementation(() => '');
createComponent({ initialValue: '' });

View File

@ -45,6 +45,7 @@ describe('Abuse Report Discussion', () => {
expect(findAbuseReportNote().props()).toMatchObject({
abuseReportId: mockAbuseReportId,
note: mockDiscussionWithNoReplies[0],
showReplyButton: true,
});
});
@ -91,5 +92,58 @@ describe('Abuse Report Discussion', () => {
isNewDiscussion: false,
});
});
it('should show the reply button only for the main comment', () => {
expect(findAbuseReportNotes().at(0).props('showReplyButton')).toBe(true);
expect(findAbuseReportNotes().at(1).props('showReplyButton')).toBe(false);
expect(findAbuseReportNotes().at(2).props('showReplyButton')).toBe(false);
});
});
describe('Replying to a comment when it has no replies', () => {
beforeEach(() => {
createComponent();
});
it('should show comment form when `startReplying` is emitted', async () => {
expect(findAbuseReportAddNote().exists()).toBe(false);
findAbuseReportNote().vm.$emit('startReplying');
await nextTick();
expect(findAbuseReportAddNote().exists()).toBe(true);
expect(findAbuseReportAddNote().props('showCommentForm')).toBe(true);
});
it('should hide the comment form when `cancelEditing` is emitted', async () => {
findAbuseReportNote().vm.$emit('startReplying');
await nextTick();
findAbuseReportAddNote().vm.$emit('cancelEditing');
await nextTick();
expect(findAbuseReportAddNote().exists()).toBe(false);
});
});
describe('Replying to a comment with replies', () => {
beforeEach(() => {
createComponent({
discussion: mockDiscussionWithReplies,
});
});
it('should show reply textarea, but not comment form', () => {
expect(findAbuseReportAddNote().exists()).toBe(true);
expect(findAbuseReportAddNote().props('showCommentForm')).toBe(false);
});
it('should show comment form when reply button on main comment is clicked', async () => {
findAbuseReportNotes().at(0).vm.$emit('startReplying');
await nextTick();
expect(findAbuseReportAddNote().props('showCommentForm')).toBe(true);
});
});
});

View File

@ -0,0 +1,46 @@
import { shallowMount } from '@vue/test-utils';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import AbuseReportNoteActions from '~/admin/abuse_report/components/notes/abuse_report_note_actions.vue';
describe('Abuse Report Note Actions', () => {
let wrapper;
const mockShowReplyButton = true;
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const createComponent = ({ showReplyButton = mockShowReplyButton } = {}) => {
wrapper = shallowMount(AbuseReportNoteActions, {
propsData: {
showReplyButton,
},
});
};
describe('Default', () => {
beforeEach(() => {
createComponent();
});
it('should show reply button', () => {
expect(findReplyButton().exists()).toBe(true);
});
it('should emit `startReplying`', () => {
findReplyButton().vm.$emit('startReplying');
expect(wrapper.emitted('startReplying')).toHaveLength(1);
});
});
describe('When `showReplyButton` is false', () => {
beforeEach(() => {
createComponent({
showReplyButton: false,
});
});
it('should not show reply button', () => {
expect(findReplyButton().exists()).toBe(false);
});
});
});

View File

@ -2,7 +2,8 @@ import { shallowMount } from '@vue/test-utils';
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
import AbuseReportNoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
import AbuseReportNoteActions from '~/admin/abuse_report/components/notes/abuse_report_note_actions.vue';
import { mockAbuseReport, mockDiscussionWithNoReplies } from '../../mock_data';
@ -10,18 +11,25 @@ describe('Abuse Report Note', () => {
let wrapper;
const mockAbuseReportId = mockAbuseReport.report.globalId;
const mockNote = mockDiscussionWithNoReplies[0];
const mockShowReplyButton = true;
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
const findNoteBody = () => wrapper.findComponent(AbuseReportNoteBody);
const findNoteActions = () => wrapper.findComponent(AbuseReportNoteActions);
const createComponent = ({ note = mockNote, abuseReportId = mockAbuseReportId } = {}) => {
const createComponent = ({
note = mockNote,
abuseReportId = mockAbuseReportId,
showReplyButton = mockShowReplyButton,
} = {}) => {
wrapper = shallowMount(AbuseReportNote, {
propsData: {
note,
abuseReportId,
showReplyButton,
},
});
};
@ -77,4 +85,19 @@ describe('Abuse Report Note', () => {
});
});
});
describe('Actions', () => {
it('should show note actions', () => {
expect(findNoteActions().exists()).toBe(true);
expect(findNoteActions().props()).toMatchObject({
showReplyButton: mockShowReplyButton,
});
});
it('should emit `startReplying`', () => {
findNoteActions().vm.$emit('startReplying');
expect(wrapper.emitted('startReplying')).toHaveLength(1);
});
});
});

View File

@ -0,0 +1,18 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountIssuesDashboardApp } from '~/issues/dashboard';
describe('IssueDashboardRoot', () => {
beforeEach(() => {
setHTMLFixture(
'<div class="js-issues-dashboard" data-has-issue-date-filter-feature="true"></div>',
);
});
afterEach(() => {
resetHTMLFixture();
});
it('mounts without errors and vue warnings', async () => {
await expect(mountIssuesDashboardApp()).resolves.toBeTruthy();
});
});

View File

@ -7,7 +7,7 @@ RSpec.describe Types::IssueTypeEnum, feature_category: :team_planning do
it 'exposes all the existing issue type values except epic' do
expect(described_class.values.keys).to match_array(
%w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE KEY_RESULT]
%w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE KEY_RESULT EPIC]
)
end
end

View File

@ -303,6 +303,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:image_path).and_return('#')
allow(helper).to receive(:url_for).and_return('#')
stub_feature_flags(issue_date_filter: false)
expected = {
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
@ -311,6 +312,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
dashboard_milestones_path: dashboard_milestones_path(format: :json),
empty_state_with_filter_svg_path: '#',
empty_state_without_filter_svg_path: '#',
has_issue_date_filter_feature: 'false',
initial_sort: current_user&.user_preference&.issues_sort,
is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
is_signed_in: current_user.present?.to_s,

View File

@ -380,6 +380,16 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
describe '.in_namespaces' do
let(:group) { create(:group) }
let!(:group_work_item) { create(:issue, :group_level, namespace: group) }
let!(:project_work_item) { create(:issue, project: reusable_project) }
subject { described_class.in_namespaces(group) }
it { is_expected.to contain_exactly(group_work_item) }
end
describe '.with_issue_type' do
let_it_be(:issue) { create(:issue, project: reusable_project) }
let_it_be(:incident) { create(:incident, project: reusable_project) }

View File

@ -39,16 +39,26 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
end
context 'when banned user has the same international dial code and phone number' do
before do
create(:phone_number_validation, user: banned_user)
context 'and the matching record has not been verified' do
before do
create(:phone_number_validation, user: banned_user)
end
it { is_expected.to eq(false) }
end
it { is_expected.to eq(true) }
context 'and the matching record has been verified' do
before do
create(:phone_number_validation, :validated, user: banned_user)
end
it { is_expected.to eq(true) }
end
end
context 'when banned user has the same international dial code and phone number, but different country code' do
before do
create(:phone_number_validation, user: banned_user, country: 'CA')
create(:phone_number_validation, :validated, user: banned_user, country: 'CA')
end
it { is_expected.to eq(true) }
@ -56,7 +66,7 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
context 'when banned user does not have the same international dial code' do
before do
create(:phone_number_validation, user: banned_user, international_dial_code: 61)
create(:phone_number_validation, :validated, user: banned_user, international_dial_code: 61)
end
it { is_expected.to eq(false) }
@ -64,7 +74,7 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
context 'when banned user does not have the same phone number' do
before do
create(:phone_number_validation, user: banned_user, phone_number: '666')
create(:phone_number_validation, :validated, user: banned_user, phone_number: '666')
end
it { is_expected.to eq(false) }
@ -72,7 +82,7 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
context 'when not-banned user has the same international dial code and phone number' do
before do
create(:phone_number_validation, user: user)
create(:phone_number_validation, :validated, user: user)
end
it { is_expected.to eq(false) }

View File

@ -79,16 +79,6 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
describe '.in_namespaces' do
let(:group) { create(:group) }
let!(:group_work_item) { create(:work_item, namespace: group) }
let!(:project_work_item) { create(:work_item, project: reusable_project) }
subject { described_class.in_namespaces(group) }
it { is_expected.to contain_exactly(group_work_item) }
end
describe '.with_confidentiality_check' do
let(:user) { create(:user) }
let!(:authored_work_item) { create(:work_item, :confidential, project: reusable_project, author: user) }

View File

@ -73,8 +73,8 @@ RSpec.describe IssuePresenter do
end
end
describe '#project_emails_disabled?' do
subject { presenter.project_emails_disabled? }
describe '#parent_emails_disabled?' do
subject { presenter.parent_emails_disabled? }
it 'returns false when emails notifications is enabled for project' do
is_expected.to be(false)
@ -87,6 +87,22 @@ RSpec.describe IssuePresenter do
it { is_expected.to be(true) }
end
context 'for group-level issue' do
let(:presented_issue) { create(:issue, :group_level, namespace: group) }
it 'returns false when email notifications are enabled for group' do
is_expected.to be(false)
end
context 'when email notifications are disabled for group' do
before do
allow(group).to receive(:emails_disabled?).and_return(true)
end
it { is_expected.to be(true) }
end
end
end
describe '#service_desk_reply_to' do

View File

@ -15,6 +15,8 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan
let_it_be(:issue2) { create(:issue, project: project2) }
let_it_be(:issue3) { create(:issue, project: project3) }
let_it_be(:group_level_issue) { create(:issue, :epic, :group_level, namespace: group1) }
let(:issue1_gid) { issue1.to_global_id.to_s }
let(:issue2_gid) { issue2.to_global_id.to_s }
let(:issues_data) { graphql_data['group']['issues']['edges'] }
@ -142,6 +144,40 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan
end
end
context 'when querying epic types' do
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => group1.full_path },
"issues(types: [EPIC]) { #{fields} }"
)
end
before_all do
group1.add_developer(current_user)
end
it 'returns group-level epics' do
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_be_empty
expect(issues_ids).to contain_exactly(group_level_issue.to_global_id.to_s)
end
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'returns no epics' do
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_be_empty
expect(issues_ids).to be_empty
end
end
end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end

View File

@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe API::ProjectEvents, feature_category: :user_profile do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: :closed, created_at: Date.new(2016, 12, 30)) }
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
let_it_be(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
let_it_be(:closed_issue_event) { create(:closed_issue_event, project: private_project, author: user, target: closed_issue, created_at: Date.new(2016, 12, 30)) }
describe 'GET /projects/:id/events' do
context 'when unauthenticated ' do
@ -27,11 +27,11 @@ RSpec.describe API::ProjectEvents, feature_category: :user_profile do
end
context 'with inaccessible events' do
let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: :closed) }
let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: :closed) }
let_it_be(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let_it_be(:confidential_issue) { create(:closed_issue, :confidential, project: public_project, author: user) }
let_it_be(:confidential_event) { create(:closed_issue_event, project: public_project, author: user, target: confidential_issue) }
let_it_be(:public_issue) { create(:closed_issue, project: public_project, author: user) }
let_it_be(:public_event) { create(:closed_issue_event, project: public_project, author: user, target: public_issue) }
it 'returns only accessible events' do
get api("/projects/#{public_project.id}/events", non_member)
@ -124,23 +124,34 @@ RSpec.describe API::ProjectEvents, feature_category: :user_profile do
end
context 'when exists some events' do
let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
let_it_be(:merge_request1) { create(:closed_merge_request, author: user, assignees: [user], source_project: private_project) }
let_it_be(:merge_request2) { create(:closed_merge_request, author: user, assignees: [user], source_project: private_project) }
let_it_be(:token) { create(:personal_access_token, user: user) }
before do
create_event(merge_request1)
end
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
end.count
# Warmup, e.g. users#last_activity_on
get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request }
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request }
end
create_event(merge_request2)
expect do
get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
end.not_to exceed_all_query_limit(control_count)
get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request }
end.to issue_same_number_of_queries_as(control).with_threshold(1)
# The extra threshold is because we need to fetch `project` for the 2nd
# event. This is because in `app/policies/issuable_policy.rb`, we fetch
# the `project` for the `target` for the `event`. It is non-trivial to
# re-use the original `project` object from `lib/api/project_events.rb`
#
# https://gitlab.com/gitlab-org/gitlab/-/issues/432823
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers

View File

@ -63,6 +63,11 @@ RSpec.shared_context 'IssuesFinder context' do
)
end
let_it_be(:group_level_item) { create(:issue, :epic, :group_level, namespace: group, author: user) }
let_it_be(:group_level_confidential_item) do
create(:issue, :confidential, :epic, :group_level, namespace: group, author: user2)
end
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }

View File

@ -63,6 +63,25 @@ RSpec.shared_context 'WorkItemsFinder context' do
)
end
let_it_be(:group_level_item) do
create(
:work_item,
:epic,
namespace: group,
author: user
)
end
let_it_be(:group_level_confidential_item) do
create(
:work_item,
:confidential,
:epic,
namespace: group,
author: user2
)
end
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }

View File

@ -269,6 +269,34 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
end
end
context 'when querying group-level items' do
let(:params) { { group_id: group.id, issue_types: %w[issue epic] } }
it 'includes group-level items' do
expect(items).to contain_exactly(item1, item5, group_level_item)
end
context 'when user has access to confidential items' do
before do
group.add_reporter(user)
end
it 'includes confidential group-level items' do
expect(items).to contain_exactly(item1, item5, group_level_item, group_level_confidential_item)
end
end
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'only returns project-level items' do
expect(items).to contain_exactly(item1, item5)
end
end
end
end
context 'filtering by author' do