Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
821ba7ce78
commit
8fc2555ccc
41 changed files with 912 additions and 364 deletions
|
@ -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'
|
||||
|
|
26
.rubocop_todo/rake/require.yml
Normal file
26
.rubocop_todo/rake/require.yml
Normal 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'
|
|
@ -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>
|
|
@ -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')"
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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')
|
|
@ -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
|
35
app/graphql/resolvers/concerns/issues/look_ahead_preloads.rb
Normal file
35
app/graphql/resolvers/concerns/issues/look_ahead_preloads.rb
Normal 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
|
26
app/graphql/resolvers/concerns/issues/sort_arguments.rb
Normal file
26
app/graphql/resolvers/concerns/issues/sort_arguments.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
48
app/graphql/resolvers/issues/base_parent_resolver.rb
Normal file
48
app/graphql/resolvers/issues/base_parent_resolver.rb
Normal 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
|
178
app/graphql/resolvers/issues/base_resolver.rb
Normal file
178
app/graphql/resolvers/issues/base_resolver.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -46,12 +46,14 @@ metadata:
|
|||
- name: project_hooks
|
||||
description: Operations related to project hooks
|
||||
- name: project_import_bitbucket
|
||||
description: Operations related to import BitBucket projects
|
||||
description: Operations related to import BitBucket projects
|
||||
- name: project_import_github
|
||||
description: Operations related to import GitHub projects
|
||||
description: Operations related to import GitHub projects
|
||||
- name: release_links
|
||||
description: Operations related to release assets (links)
|
||||
- name: releases
|
||||
description: Operations related to releases
|
||||
- name: suggestions
|
||||
description: Operations related to suggestions
|
||||
- name: unleash_api
|
||||
description: Operations related to Unleash API
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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,43 +48,74 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
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
|
||||
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(&: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&.remove_via_api!
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |show|
|
||||
Support::Waiter.wait_until { show.passed? }
|
||||
end
|
||||
|
||||
job = Resource::Job.fabricate_via_api! do |job|
|
||||
job.id = project.job_by_name(pipeline_job_name)[:id]
|
||||
job.name = pipeline_job_name
|
||||
job.project = project
|
||||
end
|
||||
|
||||
job.visit!
|
||||
|
||||
Page::Project::Job::Show.perform do |show|
|
||||
expect(show.output).to have_content(variable_custom_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
[runner, project].each(&:remove_via_api!)
|
||||
end
|
||||
|
||||
it(
|
||||
'manually creates a pipeline and uses the defined custom variable value',
|
||||
# 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
|
||||
Page::Project::Pipeline::New.perform do |new|
|
||||
new.configure_variable(value: variable_custom_value)
|
||||
new.click_run_pipeline_button
|
||||
before do
|
||||
Runtime::Feature.disable(:run_pipeline_graphql, project: project)
|
||||
end
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |show|
|
||||
Support::Waiter.wait_until { show.passed? }
|
||||
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
|
||||
|
||||
job = Resource::Job.fabricate_via_api! do |job|
|
||||
job.id = project.job_by_name(pipeline_job_name)[:id]
|
||||
job.name = pipeline_job_name
|
||||
job.project = project
|
||||
after do
|
||||
Runtime::Feature.disable(:run_pipeline_graphql, project: project)
|
||||
end
|
||||
|
||||
job.visit!
|
||||
|
||||
Page::Project::Job::Show.perform do |show|
|
||||
expect(show.output).to have_content(variable_custom_value)
|
||||
end
|
||||
it_behaves_like 'pipeline with custom variable'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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,33 +43,63 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
project.visit!
|
||||
shared_examples 'pre-filled variables form' do
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
|
||||
# Navigate to Run Pipeline page
|
||||
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
|
||||
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
|
||||
end
|
||||
project.visit!
|
||||
# Navigate to Run Pipeline page
|
||||
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
|
||||
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
|
||||
|
||||
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
|
||||
Page::Project::Pipeline::New.perform do |new|
|
||||
aggregate_failures do
|
||||
expect(new).to have_field('Input variable key', with: 'TEST1')
|
||||
expect(new).to have_field('Input variable value', with: prefill_variable_value1)
|
||||
expect(new).to have_content(prefill_variable_description1)
|
||||
# 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
|
||||
|
||||
expect(new).to have_field('Input variable key', with: 'TEST2')
|
||||
expect(new).to have_content(prefill_variable_description2)
|
||||
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')
|
||||
expect(new).to have_field('Input variable value', with: prefill_variable_value1)
|
||||
expect(new).to have_content(prefill_variable_description1)
|
||||
|
||||
expect(new).not_to have_field('Input variable key', with: 'TEST3')
|
||||
expect(new).not_to have_field('Input variable key', with: 'TEST4')
|
||||
expect(new).to have_field('Input variable key', with: 'TEST2')
|
||||
expect(new).to have_content(prefill_variable_description2)
|
||||
|
||||
expect(new).not_to have_field('Input variable key', with: 'TEST3')
|
||||
expect(new).not_to have_field('Input variable key', with: 'TEST4')
|
||||
end
|
||||
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
|
||||
|
|
84
rubocop/cop/rake/require.rb
Normal file
84
rubocop/cop/rake/require.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,10 +5,10 @@ require 'spec_helper'
|
|||
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(:user) { create(:user) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
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) }
|
||||
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
60
spec/rubocop/cop/rake/require_spec.rb
Normal file
60
spec/rubocop/cop/rake/require_spec.rb
Normal 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
|
Loading…
Reference in a new issue