Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-04 21:09:59 +00:00
parent 821ba7ce78
commit 8fc2555ccc
41 changed files with 912 additions and 364 deletions

View file

@ -848,3 +848,8 @@ Cop/SidekiqApiUsage:
- 'lib/gitlab/sidekiq_queue.rb'
- 'config/initializers/sidekiq.rb'
- 'config/initializers/forbid_sidekiq_in_transactions.rb'
Rake/Require:
Include:
- '{,ee/,jh/}lib/**/*.rake'
- 'qa/tasks/**/*.rake'

View file

@ -0,0 +1,26 @@
---
Rake/Require:
Details: grace period
Exclude:
- 'ee/lib/tasks/gitlab/spdx.rake'
- 'lib/tasks/gitlab/artifacts/migrate.rake'
- 'lib/tasks/gitlab/assets.rake'
- 'lib/tasks/gitlab/backup.rake'
- 'lib/tasks/gitlab/cleanup.rake'
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
- 'lib/tasks/gitlab/docs/redirect.rake'
- 'lib/tasks/gitlab/graphql.rake'
- 'lib/tasks/gitlab/lfs/migrate.rake'
- 'lib/tasks/gitlab/metrics_exporter.rake'
- 'lib/tasks/gitlab/openapi.rake'
- 'lib/tasks/gitlab/packages/events.rake'
- 'lib/tasks/gitlab/packages/migrate.rake'
- 'lib/tasks/gitlab/pages.rake'
- 'lib/tasks/gitlab/refresh_project_statistics_build_artifacts_size.rake'
- 'lib/tasks/gitlab/terraform/migrate.rake'
- 'lib/tasks/gitlab/tw/codeowners.rake'
- 'lib/tasks/gitlab/x509/update.rake'
- 'lib/tasks/import.rake'
- 'lib/tasks/tokens.rake'
- 'qa/tasks/ci.rake'
- 'qa/tasks/webdrivers.rake'

View file

@ -0,0 +1,54 @@
<script>
import { GlModal } from '@gitlab/ui';
import {
I18N_MODAL_TITLE,
I18N_MODAL_BODY,
I18N_MODAL_PRIMARY,
I18N_MODAL_CANCEL,
} from '../constants';
export default {
components: {
GlModal,
},
props: {
artifactName: {
type: String,
required: true,
},
deleteInProgress: {
type: Boolean,
required: true,
},
},
computed: {
actionPrimary() {
return {
text: I18N_MODAL_PRIMARY,
attributes: { variant: 'danger', loading: this.deleteInProgress },
};
},
},
actionCancel: { text: I18N_MODAL_CANCEL },
i18n: {
title: I18N_MODAL_TITLE,
body: I18N_MODAL_BODY,
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="artifact-delete-modal"
size="sm"
:title="$options.i18n.title(artifactName)"
:action-primary="actionPrimary"
:action-cancel="$options.actionCancel"
v-bind="$attrs"
v-on="$listeners"
>
{{ $options.i18n.body }}
</gl-modal>
</template>

View file

@ -16,10 +16,6 @@ export default {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
isLastRow: {
type: Boolean,
required: true,
@ -81,7 +77,6 @@ export default {
icon="remove"
:title="$options.i18n.delete"
:aria-label="$options.i18n.delete"
:loading="isLoading"
data-testid="job-artifact-row-delete-button"
@click="$emit('delete')"
/>

View file

@ -10,6 +10,7 @@ import {
ARTIFACTS_SHOWN_WITHOUT_SCROLLING,
} from '../constants';
import ArtifactRow from './artifact_row.vue';
import ArtifactDeleteModal from './artifact_delete_modal.vue';
export default {
name: 'ArtifactsTableRowDetails',
@ -17,6 +18,7 @@ export default {
DynamicScroller,
DynamicScrollerItem,
ArtifactRow,
ArtifactDeleteModal,
},
props: {
artifacts: {
@ -30,7 +32,10 @@ export default {
},
data() {
return {
isModalVisible: false,
deleteInProgress: false,
deletingArtifactId: null,
deletingArtifactName: '',
};
},
computed: {
@ -47,8 +52,22 @@ export default {
isLastRow(index) {
return index === this.artifacts.nodes.length - 1;
},
destroyArtifact(id) {
this.deletingArtifactId = id;
showModal(item) {
this.deletingArtifactId = item.id;
this.deletingArtifactName = item.name;
this.isModalVisible = true;
},
hideModal() {
this.isModalVisible = false;
},
clearModal() {
this.deletingArtifactId = null;
this.deletingArtifactName = '';
},
destroyArtifact() {
const id = this.deletingArtifactId;
this.deleteInProgress = true;
this.$apollo
.mutate({
mutation: destroyArtifactMutation,
@ -64,7 +83,8 @@ export default {
this.$emit('refetch');
})
.finally(() => {
this.deletingArtifactId = null;
this.deleteInProgress = false;
this.clearModal();
});
},
},
@ -79,11 +99,20 @@ export default {
<artifact-row
:artifact="item"
:is-last-row="isLastRow(index)"
:is-loading="item.id === deletingArtifactId"
@delete="destroyArtifact(item.id)"
@delete="showModal(item)"
/>
</dynamic-scroller-item>
</template>
</dynamic-scroller>
<artifact-delete-modal
:artifact-name="deletingArtifactName"
:visible="isModalVisible"
:delete-in-progress="deleteInProgress"
@primary="destroyArtifact"
@cancel="hideModal"
@close="hideModal"
@hide="hideModal"
@hidden="clearModal"
/>
</div>
</template>

View file

@ -1,4 +1,4 @@
import { __, s__, n__ } from '~/locale';
import { __, s__, n__, sprintf } from '~/locale';
export const JOB_STATUS_GROUP_SUCCESS = 'success';
@ -35,6 +35,14 @@ export const I18N_SIZE = __('Size');
export const I18N_CREATED = __('Created');
export const I18N_ARTIFACTS_COUNT = (count) => n__('%d file', '%d files', count);
export const I18N_MODAL_TITLE = (artifactName) =>
sprintf(s__('Artifacts|Delete %{name}?'), { name: artifactName });
export const I18N_MODAL_BODY = s__(
'Artifacts|This artifact will be permanently deleted. Any reports generated from this artifact will be empty.',
);
export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
export const I18N_MODAL_CANCEL = __('Cancel');
export const INITIAL_CURRENT_PAGE = 1;
export const INITIAL_PREVIOUS_PAGE_CURSOR = '';
export const INITIAL_NEXT_PAGE_CURSOR = '';

View file

@ -38,12 +38,9 @@ class Admin::GroupsController < Admin::ApplicationController
end
def create
@group = Group.new(group_params)
@group.name = @group.path.dup unless @group.name
@group = ::Groups::CreateService.new(current_user, group_params).execute
if @group.save
@group.add_owner(current_user)
@group.create_namespace_settings
if @group.persisted?
redirect_to [:admin, @group], notice: _('Group %{group_name} was successfully created.') % { group_name: @group.name }
else
render "new"

View file

@ -1,64 +0,0 @@
# frozen_string_literal: true
module Resolvers
class BaseIssuesResolver < BaseResolver
prepend IssueResolverArguments
argument :sort, Types::IssueSortEnum,
description: 'Sort issues by this criteria.',
required: false,
default_value: :created_desc
argument :state, Types::IssuableStateEnum,
required: false,
description: 'Current state of this issue.'
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
popularity_asc popularity_desc
label_priority_asc label_priority_desc
milestone_due_asc milestone_due_desc
escalation_status_asc escalation_status_desc].freeze
def continue_issue_resolve(parent, finder, **args)
issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) }
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.
# In these cases, we use offset pagination, so we return the correct connection.
offset_pagination(issues)
else
issues
end
end
private
def unconditional_includes
[
{
project: [:project_feature, :group]
},
:author
]
end
def preloads
{
alert_management_alert: [:alert_management_alert],
assignees: [:assignees],
participants: Issue.participant_includes,
timelogs: [:timelogs],
customer_relations_contacts: { customer_relations_contacts: [:group] },
escalation_status: [:incident_management_issuable_escalation_status]
}
end
def non_stable_cursor_sort?(sort)
NON_STABLE_CURSOR_SORTS.include?(sort)
end
end
end
Resolvers::BaseIssuesResolver.prepend_mod_with('Resolvers::BaseIssuesResolver')

View file

@ -1,183 +0,0 @@
# frozen_string_literal: true
module IssueResolverArguments
extend ActiveSupport::Concern
prepended do
include SearchArguments
include LooksAhead
argument :iid, GraphQL::Types::String,
required: false,
description: 'IID of the issue. For example, "1".'
argument :iids, [GraphQL::Types::String],
required: false,
description: 'List of IIDs of issues. For example, `["1", "2"]`.'
argument :label_name, [GraphQL::Types::String, null: true],
required: false,
description: 'Labels applied to this issue.'
argument :milestone_title, [GraphQL::Types::String, null: true],
required: false,
description: 'Milestone applied to this issue.'
argument :author_username, GraphQL::Types::String,
required: false,
description: 'Username of the author of the issue.'
argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.',
deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users assigned to the issue.'
argument :assignee_id, GraphQL::Types::String,
required: false,
description: 'ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported.'
argument :created_before, Types::TimeType,
required: false,
description: 'Issues created before this date.'
argument :created_after, Types::TimeType,
required: false,
description: 'Issues created after this date.'
argument :updated_before, Types::TimeType,
required: false,
description: 'Issues updated before this date.'
argument :updated_after, Types::TimeType,
required: false,
description: 'Issues updated after this date.'
argument :closed_before, Types::TimeType,
required: false,
description: 'Issues closed before this date.'
argument :closed_after, Types::TimeType,
required: false,
description: 'Issues closed after this date.'
argument :types, [Types::IssueTypeEnum],
as: :issue_types,
description: 'Filter issues by the given issue types.',
required: false
argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
required: false,
description: 'Filter issues by milestone ID wildcard.'
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
argument :confidential,
GraphQL::Types::Boolean,
required: false,
description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
required: false
argument :or, Types::Issues::UnionedIssueFilterInputType,
description: 'List of arguments with inclusive OR.',
required: false
argument :crm_contact_id, GraphQL::Types::String,
required: false,
description: 'ID of a contact assigned to the issues.'
argument :crm_organization_id, GraphQL::Types::String,
required: false,
description: 'ID of an organization assigned to the issues.'
end
def resolve_with_lookahead(**args)
return Issue.none if resource_parent.nil?
finder = IssuesFinder.new(current_user, prepare_finder_params(args))
continue_issue_resolve(resource_parent, finder, **args)
end
def ready?(**args)
if args[:or].present? && ::Feature.disabled?(:or_issuable_queries, resource_parent)
raise ::Gitlab::Graphql::Errors::ArgumentError, "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled."
end
args[:not] = args[:not].to_h if args[:not]
args[:or] = args[:or].to_h if args[:or]
params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
params_not_mutually_exclusive(args, mutually_exclusive_release_tag_args)
super
end
class_methods do
def resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:labelName]
complexity
end
def accept_release_tag
argument :release_tag, [GraphQL::Types::String],
required: false,
description: "Release tag associated with the issue's milestone."
argument :release_tag_wildcard_id, Types::ReleaseTagWildcardIdEnum,
required: false,
description: 'Filter issues by release tag ID wildcard.'
end
end
private
def prepare_finder_params(args)
params = super(args)
params[:not] = params[:not].to_h if params[:not]
params[:or] = params[:or].to_h if params[:or]
params[:iids] ||= [params.delete(:iid)].compact if params[:iid]
params[:attempt_project_search_optimizations] = true if params[:search].present?
prepare_author_username_params(params)
prepare_assignee_username_params(params)
prepare_release_tag_params(params)
params
end
def prepare_release_tag_params(args)
release_tag_wildcard = args.delete(:release_tag_wildcard_id)
return if release_tag_wildcard.blank?
args[:release_tag] ||= release_tag_wildcard
end
def prepare_author_username_params(args)
args[:or][:author_username] = args[:or].delete(:author_usernames) if args.dig(:or, :author_usernames).present?
end
def prepare_assignee_username_params(args)
args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present?
args[:or][:assignee_username] = args[:or].delete(:assignee_usernames) if args.dig(:or, :assignee_usernames).present?
end
def mutually_exclusive_release_tag_args
[:release_tag, :release_tag_wildcard_id]
end
def mutually_exclusive_milestone_args
[:milestone_title, :milestone_wildcard_id]
end
def mutually_exclusive_assignee_username_args
[:assignee_usernames, :assignee_username]
end
def params_not_mutually_exclusive(args, mutually_exclusive_args)
if args.slice(*mutually_exclusive_args).compact.size > 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
raise ::Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
end
end
def resource_parent
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continuing.
strong_memoize(:resource_parent) do
object.respond_to?(:sync) ? object.sync : object
end
end
end

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
module Issues
module LookAheadPreloads
extend ActiveSupport::Concern
prepended do
include ::LooksAhead
end
private
def unconditional_includes
[
{
project: [:project_feature, :group]
},
:author
]
end
def preloads
{
alert_management_alert: [:alert_management_alert],
assignees: [:assignees],
participants: Issue.participant_includes,
timelogs: [:timelogs],
customer_relations_contacts: { customer_relations_contacts: [:group] },
escalation_status: [:incident_management_issuable_escalation_status]
}
end
end
end
Issues::LookAheadPreloads.prepend_mod

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
module Issues
module SortArguments
extend ActiveSupport::Concern
NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
popularity_asc popularity_desc
label_priority_asc label_priority_desc
milestone_due_asc milestone_due_desc
escalation_status_asc escalation_status_desc].freeze
included do
argument :sort, Types::IssueSortEnum,
description: 'Sort issues by this criteria.',
required: false,
default_value: :created_desc
end
private
def non_stable_cursor_sort?(sort)
NON_STABLE_CURSOR_SORTS.include?(sort)
end
end
end

View file

@ -46,9 +46,17 @@ module SearchArguments
def prepare_search_params(args)
return args unless args[:search].present?
args[:in] = args[:in].join(',') if args[:in].present?
set_search_optimization_param(args)
args
end
def set_search_optimization_param(args)
return args unless respond_to?(:resource_parent, true) && resource_parent.present?
parent_type = resource_parent.is_a?(Project) ? :project : :group
args[:"attempt_#{parent_type}_search_optimizations"] = true
args[:in] = args[:in].join(',') if args[:in].present?
args
end

View file

@ -2,7 +2,7 @@
# rubocop:disable Graphql/ResolverType (inherited from BaseIssuesResolver)
module Resolvers
class GroupIssuesResolver < BaseIssuesResolver
class GroupIssuesResolver < Issues::BaseParentResolver
def self.issuable_collection_name
'issues'
end

View file

@ -1,17 +1,29 @@
# frozen_string_literal: true
module Resolvers
class IssueStatusCountsResolver < BaseResolver
prepend IssueResolverArguments
class IssueStatusCountsResolver < Issues::BaseResolver
type Types::IssueStatusCountsType, null: true
accept_release_tag
extras [:lookahead]
def resolve(**args)
return Issue.none if resource_parent.nil?
def continue_issue_resolve(parent, finder, **args)
finder.parent_param = parent
apply_lookahead(Gitlab::IssuablesCountForState.new(finder, parent))
finder = IssuesFinder.new(current_user, prepare_finder_params(args))
finder.parent_param = resource_parent
Gitlab::IssuablesCountForState.new(finder, resource_parent)
end
private
def resource_parent
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continuing.
strong_memoize(:resource_parent) do
object.respond_to?(:sync) ? object.sync : object
end
end
end
end

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
module Resolvers
module Issues
class BaseParentResolver < Issues::BaseResolver
prepend ::Issues::LookAheadPreloads
include ::Issues::SortArguments
argument :state, Types::IssuableStateEnum,
required: false,
description: 'Current state of this issue.'
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
def resolve_with_lookahead(**args)
return Issue.none if resource_parent.nil?
finder = IssuesFinder.new(current_user, prepare_finder_params(args))
issues = Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all do |q|
apply_lookahead(q)
end
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.
# In these cases, we use offset pagination, so we return the correct connection.
offset_pagination(issues)
else
issues
end
end
private
def resource_parent
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continuing.
strong_memoize(:resource_parent) do
object.respond_to?(:sync) ? object.sync : object
end
end
end
end
end
Resolvers::Issues::BaseParentResolver.prepend_mod

View file

@ -0,0 +1,178 @@
# frozen_string_literal: true
module Resolvers
module Issues
# rubocop:disable Graphql/ResolverType
class BaseResolver < Resolvers::BaseResolver
include SearchArguments
argument :assignee_id, GraphQL::Types::String,
required: false,
description: 'ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported.'
argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.',
deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users assigned to the issue.'
argument :author_username, GraphQL::Types::String,
required: false,
description: 'Username of the author of the issue.'
argument :closed_after, Types::TimeType,
required: false,
description: 'Issues closed after this date.'
argument :closed_before, Types::TimeType,
required: false,
description: 'Issues closed before this date.'
argument :confidential,
GraphQL::Types::Boolean,
required: false,
description: 'Filter for confidential issues. If "false", excludes confidential issues.' \
' If "true", returns only confidential issues.'
argument :created_after, Types::TimeType,
required: false,
description: 'Issues created after this date.'
argument :created_before, Types::TimeType,
required: false,
description: 'Issues created before this date.'
argument :crm_contact_id, GraphQL::Types::String,
required: false,
description: 'ID of a contact assigned to the issues.'
argument :crm_organization_id, GraphQL::Types::String,
required: false,
description: 'ID of an organization assigned to the issues.'
argument :iid, GraphQL::Types::String,
required: false,
description: 'IID of the issue. For example, "1".'
argument :iids, [GraphQL::Types::String],
required: false,
description: 'List of IIDs of issues. For example, `["1", "2"]`.'
argument :label_name, [GraphQL::Types::String, { null: true }],
required: false,
description: 'Labels applied to this issue.'
argument :milestone_title, [GraphQL::Types::String, { null: true }],
required: false,
description: 'Milestone applied to this issue.'
argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
required: false,
description: 'Filter issues by milestone ID wildcard.'
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user.' \
' Wildcard values "NONE" and "ANY" are supported.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
required: false
argument :or, Types::Issues::UnionedIssueFilterInputType,
description: 'List of arguments with inclusive OR.',
required: false
argument :types, [Types::IssueTypeEnum],
as: :issue_types,
description: 'Filter issues by the given issue types.',
required: false
argument :updated_after, Types::TimeType,
required: false,
description: 'Issues updated after this date.'
argument :updated_before, Types::TimeType,
required: false,
description: 'Issues updated before this date.'
class << self
def resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:labelName]
complexity
end
def accept_release_tag
argument :release_tag, [GraphQL::Types::String],
required: false,
description: "Release tag associated with the issue's milestone."
argument :release_tag_wildcard_id, Types::ReleaseTagWildcardIdEnum,
required: false,
description: 'Filter issues by release tag ID wildcard.'
end
end
def ready?(**args)
if args[:or].present? && ::Feature.disabled?(:or_issuable_queries, resource_parent)
raise ::Gitlab::Graphql::Errors::ArgumentError,
"'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled."
end
args[:not] = args[:not].to_h if args[:not]
args[:or] = args[:or].to_h if args[:or]
params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
params_not_mutually_exclusive(args, mutually_exclusive_release_tag_args)
super
end
private
def prepare_finder_params(args)
params = super(args)
params[:not] = params[:not].to_h if params[:not]
params[:or] = params[:or].to_h if params[:or]
params[:iids] ||= [params.delete(:iid)].compact if params[:iid]
prepare_author_username_params(params)
prepare_assignee_username_params(params)
prepare_release_tag_params(params)
params
end
def prepare_release_tag_params(args)
release_tag_wildcard = args.delete(:release_tag_wildcard_id)
return if release_tag_wildcard.blank?
args[:release_tag] ||= release_tag_wildcard
end
def prepare_author_username_params(args)
args[:or][:author_username] = args[:or].delete(:author_usernames) if args.dig(:or, :author_usernames).present?
end
def prepare_assignee_username_params(args)
args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
if args.dig(:or, :assignee_usernames).present?
args[:or][:assignee_username] = args[:or].delete(:assignee_usernames)
end
return unless args.dig(:not, :assignee_usernames).present?
args[:not][:assignee_username] = args[:not].delete(:assignee_usernames)
end
def mutually_exclusive_release_tag_args
[:release_tag, :release_tag_wildcard_id]
end
def mutually_exclusive_milestone_args
[:milestone_title, :milestone_wildcard_id]
end
def mutually_exclusive_assignee_username_args
[:assignee_usernames, :assignee_username]
end
def params_not_mutually_exclusive(args, mutually_exclusive_args)
return unless args.slice(*mutually_exclusive_args).compact.size > 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
raise ::Gitlab::Graphql::Errors::ArgumentError,
"only one of [#{arg_str}] arguments is allowed at the same time."
end
end
# rubocop:enable Graphql/ResolverType
end
end
Resolvers::Issues::BaseResolver.prepend_mod

View file

@ -2,7 +2,7 @@
# rubocop:disable Graphql/ResolverType (inherited from BaseIssuesResolver)
module Resolvers
class IssuesResolver < BaseIssuesResolver
class IssuesResolver < Issues::BaseParentResolver
accept_release_tag
end
end

View file

@ -241,7 +241,6 @@ module Types
Types::IssueStatusCountsType,
null: true,
description: 'Counts of issues by status for the project.',
extras: [:lookahead],
resolver: Resolvers::IssueStatusCountsResolver
field :milestones, Types::MilestoneType.connection_type,

View file

@ -66,7 +66,7 @@ class MergeRequestNoteableEntity < IssuableEntity
expose :project_id
expose :archived_project_docs_path, if: -> (merge_request) { merge_request.project.archived? } do |merge_request|
help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project')
help_page_path('user/project/settings/index.md', anchor: 'archive-a-project')
end
private

View file

@ -55,3 +55,5 @@ metadata:
description: Operations related to releases
- name: suggestions
description: Operations related to suggestions
- name: unleash_api
description: Operations related to Unleash API

View file

@ -236,3 +236,26 @@ To temporarily change the statement timeout:
1. Perform the action for which you need a different timeout
(for example the backup or the Rails command).
1. Revert the edit in `/var/opt/gitlab/gitlab-rails/etc/database.yml`.
## Troubleshooting
### Database is not accepting commands to avoid wraparound data loss
This error likely means that AUTOVACUUM is failing to complete its run:
```plaintext
ERROR: database is not accepting commands to avoid wraparound data loss in database "gitlabhq_production"
```
To resolve the error, run `VACUUM` manually:
1. Stop GitLab with the command `gitlab-ctl stop`.
1. Place the database in single-user mode with the command:
```shell
/opt/gitlab/embedded/bin/postgres --single -D /var/opt/gitlab/postgresql/data gitlabhq_production
```
1. In the `backend>` prompt, run `VACUUM;`. This command can take several minutes to complete.
1. Wait for the command to complete, then press <kbd>Control</kbd> + <kbd>D</kbd> to exit.
1. Start GitLab with the command `gitlab-ctl start`.

View file

@ -16933,9 +16933,14 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissuestatuscountscrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
| <a id="projectissuestatuscountscrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
| <a id="projectissuestatuscountsepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
| <a id="projectissuestatuscountshealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the issue, "none" and "any" values are supported. |
| <a id="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
| <a id="projectissuestatuscountsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |
| <a id="projectissuestatuscountsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="projectissuestatuscountsincludesubepics"></a>`includeSubepics` | [`Boolean`](#boolean) | Whether to include subepics when filtering issues by epicId. |
| <a id="projectissuestatuscountsiterationid"></a>`iterationId` | [`[ID]`](#id) | List of iteration Global IDs applied to the issue. |
| <a id="projectissuestatuscountsiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. |
| <a id="projectissuestatuscountslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
| <a id="projectissuestatuscountsmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. |
| <a id="projectissuestatuscountsmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. |
@ -16948,6 +16953,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <a id="projectissuestatuscountstypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter issues by the given issue types. |
| <a id="projectissuestatuscountsupdatedafter"></a>`updatedAfter` | [`Time`](#time) | Issues updated after this date. |
| <a id="projectissuestatuscountsupdatedbefore"></a>`updatedBefore` | [`Time`](#time) | Issues updated before this date. |
| <a id="projectissuestatuscountsweight"></a>`weight` | [`String`](#string) | Weight applied to the issue, "none" and "any" values are supported. |
##### `Project.issues`

View file

@ -191,6 +191,7 @@ module API
mount ::API::ImportGithub
mount ::API::Metadata
mount ::API::MergeRequestDiffs
mount ::API::PersonalAccessTokens::SelfInformation
mount ::API::ProjectHooks
mount ::API::ProjectRepositoryStorageMoves
mount ::API::Releases
@ -204,6 +205,7 @@ module API
mount ::API::Submodules
mount ::API::Suggestions
mount ::API::Tags
mount ::API::Unleash
mount ::API::UserCounts
add_open_api_documentation!
@ -286,7 +288,6 @@ module API
mount ::API::PackageFiles
mount ::API::Pages
mount ::API::PagesDomains
mount ::API::PersonalAccessTokens::SelfInformation
mount ::API::PersonalAccessTokens
mount ::API::ProjectClusters
mount ::API::ProjectContainerRepositories
@ -322,7 +323,6 @@ module API
mount ::API::Terraform::StateVersion
mount ::API::Todos
mount ::API::Topics
mount ::API::Unleash
mount ::API::UsageData
mount ::API::UsageDataNonSqlMetrics
mount ::API::UsageDataQueries

View file

@ -3,12 +3,12 @@
module API
module Entities
class PersonalAccessToken < Grape::Entity
expose :id, documentation: { type: 'string', example: 2 }
expose :id, documentation: { type: 'integer', example: 2 }
expose :name, documentation: { type: 'string', example: 'John Doe' }
expose :revoked, documentation: { type: 'boolean' }
expose :created_at, documentation: { type: 'dateTime' }
expose :scopes, documentation: { type: 'array', example: ['api'] }
expose :user_id, documentation: { type: 'string', example: 3 }
expose :user_id, documentation: { type: 'integer', example: 3 }
expose :last_used_at, documentation: { type: 'dateTime', example: '2020-08-31T15:53:00.073Z' }
expose :active?, as: :active, documentation: { type: 'boolean' }
expose :expires_at, documentation:

View file

@ -93,7 +93,7 @@ module API
params :extended_file_params do
use :simple_file_params
requires :content, type: String, desc: 'File content'
optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
optional :encoding, type: String, values: %w[base64 text], default: 'text', desc: 'File encoding'
optional :last_commit_id, type: String, desc: 'Last known commit id for this file'
optional :execute_filemode, type: Boolean, desc: 'Enable / Disable the executable flag on the file path'
end

View file

@ -17,10 +17,28 @@ module API
before { authenticate! }
resource :personal_access_tokens do
desc "Get single personal access token" do
detail 'Get the details of a personal access token by passing it to the API in a header'
success code: 200, model: Entities::PersonalAccessToken
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 404, message: 'Not found' }
]
tags %w[personal_access_tokens]
end
get 'self' do
present access_token, with: Entities::PersonalAccessToken
end
desc "Revoke a personal access token" do
detail 'Revoke a personal access token by passing it to the API in a header'
success code: 204
failure [
{ code: 400, message: 'Bad Request' }
]
tags %w[personal_access_tokens]
end
delete 'self' do
revoke_token(access_token)
end

View file

@ -4,14 +4,16 @@ module API
class Unleash < ::API::Base
include PaginationParams
unleash_tags = %w[unleash_api]
feature_category :feature_flags
namespace :feature_flags do
resource :unleash, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :project_id, type: String, desc: 'The ID of a project'
optional :instance_id, type: String, desc: 'The Instance ID of Unleash Client'
optional :app_name, type: String, desc: 'The Application Name of Unleash Client'
optional :instance_id, type: String, desc: 'The instance ID of Unleash Client'
optional :app_name, type: String, desc: 'The application name of Unleash Client'
end
route_param :project_id do
before do
@ -23,7 +25,10 @@ module API
status :ok
end
desc 'Get a list of features (deprecated, v2 client support)'
desc 'Get a list of features (deprecated, v2 client support)' do
is_array true
tags unleash_tags
end
get 'features', urgency: :low do
if ::Feature.enabled?(:cache_unleash_client_api, project)
present_feature_flags
@ -35,7 +40,10 @@ module API
# We decrease the urgency of this endpoint until the maxmemory issue of redis-cache has been resolved.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/365575#note_1033611872 for more information.
desc 'Get a list of features'
desc 'Get a list of features' do
is_array true
tags unleash_tags
end
get 'client/features', urgency: :low do
if ::Feature.enabled?(:cache_unleash_client_api, project)
present_feature_flags

View file

@ -5323,6 +5323,15 @@ msgstr ""
msgid "Artifacts|Browse"
msgstr ""
msgid "Artifacts|Delete %{name}?"
msgstr ""
msgid "Artifacts|Delete artifact"
msgstr ""
msgid "Artifacts|This artifact will be permanently deleted. Any reports generated from this artifact will be empty."
msgstr ""
msgid "Artifacts|Total artifacts size"
msgstr ""
@ -45172,6 +45181,9 @@ msgstr ""
msgid "Vulnerability|Scanner Provider"
msgstr ""
msgid "Vulnerability|Scanner:"
msgstr ""
msgid "Vulnerability|Security Audit"
msgstr ""

View file

@ -1,8 +1,11 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_authoring do
describe 'Pipeline with customizable variable' do
RSpec.describe 'Verify', :runner do
describe 'Pipeline with customizable variable', feature_flag: {
name: :run_pipeline_graphql,
scope: :project
} do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
let(:pipeline_job_name) { 'customizable-variable' }
let(:variable_custom_value) { 'Custom Foo' }
@ -45,23 +48,25 @@ module QA
end
end
shared_examples 'pipeline with custom variable' do
before do
Flow::Login.sign_in
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform do |index|
index.click_run_pipeline_button
end
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
# Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again.
# TODO: Investigate alternatives to deal with cache implementation
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233
page.refresh
end
after do
[runner, project].each(&:remove_via_api!)
runner&.remove_via_api!
end
it(
'manually creates a pipeline and uses the defined custom variable value',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/361814'
) do
it 'manually creates a pipeline and uses the defined custom variable value' do
Page::Project::Pipeline::New.perform do |new|
new.configure_variable(value: variable_custom_value)
new.click_run_pipeline_button
@ -84,5 +89,34 @@ module QA
end
end
end
# TODO: Clean up tests when run_pipeline_graphql is enabled
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310
context(
'with feature flag disabled',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/361814'
) do
before do
Runtime::Feature.disable(:run_pipeline_graphql, project: project)
end
it_behaves_like 'pipeline with custom variable'
end
context(
'with feature flag enabled',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378975'
) do
before do
Runtime::Feature.enable(:run_pipeline_graphql, project: project)
end
after do
Runtime::Feature.disable(:run_pipeline_graphql, project: project)
end
it_behaves_like 'pipeline with custom variable'
end
end
end
end

View file

@ -2,7 +2,10 @@
module QA
RSpec.describe 'Verify' do
describe 'Pipeline with prefill variables', product_group: :pipeline_authoring do
describe 'Pipeline with prefill variables', feature_flag: {
name: :run_pipeline_graphql,
scope: :project
} do
let(:prefill_variable_description1) { Faker::Lorem.sentence }
let(:prefill_variable_value1) { Faker::Lorem.word }
let(:prefill_variable_description2) { Faker::Lorem.sentence }
@ -40,19 +43,22 @@ module QA
end
end
shared_examples 'pre-filled variables form' do
before do
Flow::Login.sign_in
project.visit!
project.visit!
# Navigate to Run Pipeline page
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
# Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again.
# TODO: Investigate alternatives to deal with cache implementation
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233
page.refresh
end
it(
'shows only variables with description as prefill variables on the run pipeline page',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/371204'
) do
it 'shows only variables with description as prefill variables on the run pipeline page' do
Page::Project::Pipeline::New.perform do |new|
aggregate_failures do
expect(new).to have_field('Input variable key', with: 'TEST1')
@ -68,5 +74,32 @@ module QA
end
end
end
# TODO: Clean up tests when run_pipeline_graphql is enabled
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310
context(
'with feature flag disabled',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/371204'
) do
before do
Runtime::Feature.disable(:run_pipeline_graphql, project: project)
end
it_behaves_like 'pre-filled variables form'
end
context 'with feature flag enabled',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/378977' do
before do
Runtime::Feature.enable(:run_pipeline_graphql, project: project)
end
after do
Runtime::Feature.disable(:run_pipeline_graphql, project: project)
end
it_behaves_like 'pre-filled variables form'
end
end
end
end

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Rake
# Flag global `require`s or `require_relative`s in rake files.
#
# Load dependencies lazily in `task` definitions if possible.
#
# @example
# # bad
#
# require_relative 'gitlab/json'
# require 'json'
#
# task :parse_json do
# Gitlab::Json.parse(...)
# end
#
# # good
#
# task :parse_json do
# require_relative 'gitlab/json'
# require 'json'
#
# Gitlab::Json.parse(...)
# end
#
# RSpec::Core::RakeTask.new(:parse_json) do |t, args|
# require_relative 'gitlab/json'
# require 'json'
#
# Gitlab::Json.parse(...)
# end
#
# # Requiring files which contain the word `task` is allowed.
# require 'some_gem/rake_task'
# require 'some_gem/rake_tasks'
#
# SomeGem.define_tasks
#
# # Loading in method definition as well.
# def load_deps
# require 'json'
# end
#
# task :parse_json
# load_deps
# end
#
class Require < RuboCop::Cop::Base
MSG = 'Load dependencies inside `task` definitions if possible.'
METHODS = %i[require require_relative].freeze
RESTRICT_ON_SEND = METHODS
def_node_matcher :require_method, <<~PATTERN
(send nil? ${#{METHODS.map(&:inspect).join(' ')}} $_)
PATTERN
def on_send(node)
method, file = require_method(node)
return unless method
return if requires_task?(file)
return if inside_block_or_method?(node)
add_offense(node)
end
private
# Allow `require "foo/rake_task"`
def requires_task?(file)
file.source.include?('task')
end
def inside_block_or_method?(node)
node.each_ancestor(:block, :def).any?
end
end
end
end
end

View file

@ -43,5 +43,13 @@ RSpec.describe Admin::GroupsController do
post :create, params: { group: { path: 'test', name: 'test', admin_note_attributes: { note: 'test' } } }
end.to change { Namespace::AdminNote.count }.by(1)
end
it 'delegates to Groups::CreateService service instance' do
expect_next_instance_of(::Groups::CreateService) do |service|
expect(service).to receive(:execute).once.and_call_original
end
post :create, params: { group: { path: 'test', name: 'test' } }
end
end
end

View file

@ -680,7 +680,7 @@ RSpec.describe 'Pipelines', :js do
end
context 'when variables are specified' do
it 'creates a new pipeline with variables', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
it 'creates a new pipeline with variables' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
find("[data-testid='pipeline-form-ci-variable-value']").set('value')
@ -708,7 +708,7 @@ RSpec.describe 'Pipelines', :js do
it { expect(page).to have_content('Missing CI config file') }
it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
stub_ci_pipeline_to_return_yaml_file
expect do
@ -722,6 +722,7 @@ RSpec.describe 'Pipelines', :js do
# Run Pipeline form with REST endpoints
# TODO: Clean up tests when run_pipeline_graphql is enabled
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310
context 'with feature flag disabled' do
before do
stub_feature_flags(run_pipeline_graphql: false)

View file

@ -1,13 +1,15 @@
import Vue, { nextTick } from 'vue';
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
import ArtifactRow from '~/artifacts/components/artifact_row.vue';
import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
import { I18N_DESTROY_ERROR } from '~/artifacts/constants';
import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
import { createAlert } from '~/flash';
jest.mock('~/flash');
@ -21,6 +23,8 @@ describe('ArtifactsTableRowDetails component', () => {
let wrapper;
let requestHandlers;
const findModal = () => wrapper.findComponent(GlModal);
const createComponent = (
handlers = {
destroyArtifactMutation: jest.fn(),
@ -55,38 +59,36 @@ describe('ArtifactsTableRowDetails component', () => {
[0, 1, 2].forEach((index) => {
expect(wrapper.findAllComponents(ArtifactRow).at(index).props()).toMatchObject({
artifact: artifacts.nodes[index],
isLoading: false,
});
});
});
});
describe('when an artifact row emits the delete event', () => {
it('sets isLoading to true for that row', async () => {
describe('when the artifact row emits the delete event', () => {
it('shows the artifact delete modal', async () => {
createComponent();
await waitForPromises();
wrapper.findComponent(ArtifactRow).vm.$emit('delete');
expect(findModal().props('visible')).toBe(false);
await nextTick();
await wrapper.findComponent(ArtifactRow).vm.$emit('delete');
[
{ index: 0, expectedLoading: true },
{ index: 1, expectedLoading: false },
].forEach(({ index, expectedLoading }) => {
expect(wrapper.findAllComponents(ArtifactRow).at(index).props('isLoading')).toBe(
expectedLoading,
);
expect(findModal().props('visible')).toBe(true);
expect(findModal().props('title')).toBe(I18N_MODAL_TITLE(artifacts.nodes[0].name));
});
});
describe('when the artifact delete modal emits its primary event', () => {
it('triggers the destroyArtifact GraphQL mutation', async () => {
createComponent();
await waitForPromises();
wrapper.findComponent(ArtifactRow).vm.$emit('delete');
wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
expect(requestHandlers.destroyArtifactMutation).toHaveBeenCalled();
expect(requestHandlers.destroyArtifactMutation).toHaveBeenCalledWith({
id: artifacts.nodes[0].id,
});
});
it('displays a flash message and refetches artifacts when the mutation fails', async () => {
@ -98,10 +100,23 @@ describe('ArtifactsTableRowDetails component', () => {
expect(wrapper.emitted('refetch')).toBeUndefined();
wrapper.findComponent(ArtifactRow).vm.$emit('delete');
wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: I18N_DESTROY_ERROR });
expect(wrapper.emitted('refetch')).toBeDefined();
});
});
describe('when the artifact delete modal is cancelled', () => {
it('does not trigger the destroyArtifact GraphQL mutation', async () => {
createComponent();
await waitForPromises();
wrapper.findComponent(ArtifactRow).vm.$emit('delete');
wrapper.findComponent(ArtifactDeleteModal).vm.$emit('cancel');
expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled();
});
});
});

View file

@ -8,7 +8,6 @@ import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants';
import { totalArtifactsSizeForJob } from '~/artifacts/utils';
@ -68,7 +67,6 @@ describe('JobArtifactsTable component', () => {
const createComponent = (
handlers = {
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
destroyArtifactMutation: jest.fn(),
},
data = {},
) => {
@ -76,7 +74,6 @@ describe('JobArtifactsTable component', () => {
wrapper = mountExtended(JobArtifactsTable, {
apolloProvider: createMockApollo([
[getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
[destroyArtifactMutation, requestHandlers.destroyArtifactMutation],
]),
provide: { projectPath: 'project/path' },
data() {

View file

@ -597,6 +597,7 @@ RSpec.describe SearchHelper do
'<script type="text/javascript">alert(\'Another XSS\');</script> test' | ' <span class="gl-text-gray-900 gl-font-weight-bold">test</span>'
'Lorem test ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec.' | 'Lorem <span class="gl-text-gray-900 gl-font-weight-bold">test</span> ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Don...'
'<img src="https://random.foo.com/test.png" width="128" height="128" />some image' | 'some image'
'<h2 data-sourcepos="11:1-11:26" dir="auto"><a id="user-content-additional-information" class="anchor" href="#additional-information" aria-hidden="true"></a>Additional information test:</h2><textarea data-update-url="/freepascal.org/fpc/source/-/issues/6163.json" dir="auto" data-testid="textarea" class="hidden js-task-list-field"></textarea>' | '<a class="anchor" href="#additional-information"></a>Additional information <span class="gl-text-gray-900 gl-font-weight-bold">test</span>:'
end
with_them do

View file

@ -12,12 +12,24 @@ RSpec.describe Banzai::ReferenceParser::CommitParser do
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
context 'when the link has a data-project attribute' do
before do
link['data-commit'] = 123
link['data-project'] = project.id.to_s
end
it_behaves_like "referenced feature visibility", "repository"
it 'includes the link if can_read_reference? returns true' do
expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to contain_exactly(link)
end
it 'excludes the link if can_read_reference? returns false' do
expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to be_empty
end
end
end

View file

@ -12,12 +12,24 @@ RSpec.describe Banzai::ReferenceParser::CommitRangeParser do
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
context 'when the link has a data-project attribute' do
before do
link['data-commit-range'] = '123..456'
link['data-project'] = project.id.to_s
end
it_behaves_like "referenced feature visibility", "repository"
it 'includes the link if can_read_reference? returns true' do
expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to contain_exactly(link)
end
it 'excludes the link if can_read_reference? returns false' do
expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(false)
expect(subject.nodes_visible_to_user(user, [link])).to be_empty
end
end
end
@ -136,4 +148,22 @@ RSpec.describe Banzai::ReferenceParser::CommitRangeParser do
end
end
end
context 'when checking commits ranges on another project' do
let!(:control_links) do
[commit_range_link]
end
let!(:actual_links) do
control_links + [commit_range_link, commit_range_link]
end
def commit_range_link
project = create(:project, :repository, :public)
Nokogiri::HTML.fragment(%(<a data-commit-range="123...456" data-project="#{project.id}"></a>)).children[0]
end
it_behaves_like 'no project N+1 queries'
end
end

View file

@ -6,7 +6,7 @@ RSpec.describe Banzai::ReferenceParser::IssueParser do
include ReferenceParserHelpers
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be_with_reload(:project) { create(:project, :public, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }

View file

@ -1253,4 +1253,35 @@ RSpec.describe API::Files do
expect(json_response['content']).to eq(put_params[:content])
end
end
describe 'POST /projects/:id/repository/files with text encoding' do
let(:file_path) { 'test%2Etext' }
let(:put_params) do
{
branch: 'master',
content: 'test',
commit_message: 'Text file',
encoding: 'text'
}
end
let(:get_params) do
{
ref: 'master'
}
end
before do
post api(route(file_path), user), params: put_params
end
it 'returns base64-encoded text file' do
get api(route(file_path), user), params: get_params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
expect(json_response['file_name']).to eq(CGI.unescape(file_path))
expect(Base64.decode64(json_response['content'])).to eq("test")
end
end
end

View file

@ -0,0 +1,60 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/rake/require'
RSpec.describe RuboCop::Cop::Rake::Require do
let(:msg) { described_class::MSG }
it 'registers an offenses for require methods' do
expect_offense(<<~RUBY)
require 'json'
^^^^^^^^^^^^^^ #{msg}
require_relative 'gitlab/json'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
RUBY
end
it 'does not register offense inside `task` definition' do
expect_no_offenses(<<~RUBY)
task :parse do
require 'json'
end
namespace :some do
task parse: :env do
require_relative 'gitlab/json'
end
end
RUBY
end
it 'does not register offense inside a block definition' do
expect_no_offenses(<<~RUBY)
RSpec::Core::RakeTask.new(:parse_json) do |t, args|
require 'json'
end
RUBY
end
it 'does not register offense inside a method definition' do
expect_no_offenses(<<~RUBY)
def load_deps
require 'json'
end
task :parse do
load_deps
end
RUBY
end
it 'does not register offense when require task related files' do
expect_no_offenses(<<~RUBY)
require 'rubocop/rake_tasks'
require 'gettext_i18n_rails/tasks'
require_relative '../../rubocop/check_graceful_task'
RUBY
end
end