diff --git a/.rubocop.yml b/.rubocop.yml index ad5bb5e9a13..e2718863a1c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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' diff --git a/.rubocop_todo/rake/require.yml b/.rubocop_todo/rake/require.yml new file mode 100644 index 00000000000..5042f0d504e --- /dev/null +++ b/.rubocop_todo/rake/require.yml @@ -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' diff --git a/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue b/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue new file mode 100644 index 00000000000..14edd73824e --- /dev/null +++ b/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/assets/javascripts/artifacts/components/artifact_row.vue b/app/assets/javascripts/artifacts/components/artifact_row.vue index b6cea6a04e3..8c03db2acd1 100644 --- a/app/assets/javascripts/artifacts/components/artifact_row.vue +++ b/app/assets/javascripts/artifacts/components/artifact_row.vue @@ -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')" /> diff --git a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue index 089bfd80222..4a826d0d462 100644 --- a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue +++ b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue @@ -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 { + diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/artifacts/constants.js index 9ed0821ac2d..5fcc4f2b76e 100644 --- a/app/assets/javascripts/artifacts/constants.js +++ b/app/assets/javascripts/artifacts/constants.js @@ -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 = ''; diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index f3c4244269d..1395d4bb3b7 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -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" diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb deleted file mode 100644 index 6357132705e..00000000000 --- a/app/graphql/resolvers/base_issues_resolver.rb +++ /dev/null @@ -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') diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb deleted file mode 100644 index 69a32d67330..00000000000 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ /dev/null @@ -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 diff --git a/app/graphql/resolvers/concerns/issues/look_ahead_preloads.rb b/app/graphql/resolvers/concerns/issues/look_ahead_preloads.rb new file mode 100644 index 00000000000..c6e32be245d --- /dev/null +++ b/app/graphql/resolvers/concerns/issues/look_ahead_preloads.rb @@ -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 diff --git a/app/graphql/resolvers/concerns/issues/sort_arguments.rb b/app/graphql/resolvers/concerns/issues/sort_arguments.rb new file mode 100644 index 00000000000..70ae6bd8a5b --- /dev/null +++ b/app/graphql/resolvers/concerns/issues/sort_arguments.rb @@ -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 diff --git a/app/graphql/resolvers/concerns/search_arguments.rb b/app/graphql/resolvers/concerns/search_arguments.rb index 95c6dbf7497..ccc012f2bf9 100644 --- a/app/graphql/resolvers/concerns/search_arguments.rb +++ b/app/graphql/resolvers/concerns/search_arguments.rb @@ -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 diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb index 05c5e803539..e02e4277cb4 100644 --- a/app/graphql/resolvers/group_issues_resolver.rb +++ b/app/graphql/resolvers/group_issues_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/issue_status_counts_resolver.rb b/app/graphql/resolvers/issue_status_counts_resolver.rb index db5c91daac2..92cda77d717 100644 --- a/app/graphql/resolvers/issue_status_counts_resolver.rb +++ b/app/graphql/resolvers/issue_status_counts_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/issues/base_parent_resolver.rb b/app/graphql/resolvers/issues/base_parent_resolver.rb new file mode 100644 index 00000000000..6308e56f049 --- /dev/null +++ b/app/graphql/resolvers/issues/base_parent_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/issues/base_resolver.rb b/app/graphql/resolvers/issues/base_resolver.rb new file mode 100644 index 00000000000..88579b09482 --- /dev/null +++ b/app/graphql/resolvers/issues/base_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 4b52ef61d57..411cf0cdb45 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -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 diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 8374a89ad35..c920abe460c 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -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, diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb index 07d7d19d1f3..306bac7daae 100644 --- a/app/serializers/merge_request_noteable_entity.rb +++ b/app/serializers/merge_request_noteable_entity.rb @@ -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 diff --git a/config/open_api.yml b/config/open_api.yml index ed454dd52dd..f043932c71a 100644 --- a/config/open_api.yml +++ b/config/open_api.yml @@ -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 diff --git a/doc/administration/troubleshooting/postgresql.md b/doc/administration/troubleshooting/postgresql.md index 24d7de40b2b..e392bdffe30 100644 --- a/doc/administration/troubleshooting/postgresql.md +++ b/doc/administration/troubleshooting/postgresql.md @@ -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 Control + D to exit. +1. Start GitLab with the command `gitlab-ctl start`. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index a3b0c6635a4..58286e0c12a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -16933,9 +16933,14 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | `createdBefore` | [`Time`](#time) | Issues created before this date. | | `crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | | `crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| `epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | +| `healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the issue, "none" and "any" values are supported. | | `iid` | [`String`](#string) | IID of the issue. For example, "1". | | `iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. | | `in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. | +| `includeSubepics` | [`Boolean`](#boolean) | Whether to include subepics when filtering issues by epicId. | +| `iterationId` | [`[ID]`](#id) | List of iteration Global IDs applied to the issue. | +| `iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. | | `labelName` | [`[String]`](#string) | Labels applied to this issue. | | `milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. | | `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | @@ -16948,6 +16953,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | `types` | [`[IssueType!]`](#issuetype) | Filter issues by the given issue types. | | `updatedAfter` | [`Time`](#time) | Issues updated after this date. | | `updatedBefore` | [`Time`](#time) | Issues updated before this date. | +| `weight` | [`String`](#string) | Weight applied to the issue, "none" and "any" values are supported. | ##### `Project.issues` diff --git a/lib/api/api.rb b/lib/api/api.rb index fa10d796e00..596909bcea2 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -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 diff --git a/lib/api/entities/personal_access_token.rb b/lib/api/entities/personal_access_token.rb index 305412d3dcc..3ec91ca5fc9 100644 --- a/lib/api/entities/personal_access_token.rb +++ b/lib/api/entities/personal_access_token.rb @@ -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: diff --git a/lib/api/files.rb b/lib/api/files.rb index fd574ca865b..1b3961e4aac 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -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 diff --git a/lib/api/personal_access_tokens/self_information.rb b/lib/api/personal_access_tokens/self_information.rb index 89850614f94..5735fe49f33 100644 --- a/lib/api/personal_access_tokens/self_information.rb +++ b/lib/api/personal_access_tokens/self_information.rb @@ -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 diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb index a8304419827..949f4a46a1b 100644 --- a/lib/api/unleash.rb +++ b/lib/api/unleash.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 041ff029a4b..f240c3ccfd2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb index 1db6f48a404..4a0a8be3659 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb @@ -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 diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb index 7eec0f8870a..c4ce916d47d 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb @@ -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 diff --git a/rubocop/cop/rake/require.rb b/rubocop/cop/rake/require.rb new file mode 100644 index 00000000000..e3e1696943d --- /dev/null +++ b/rubocop/cop/rake/require.rb @@ -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 diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index 37cb0a1f289..6085f0e1239 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -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 diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index eabbcd5e38e..b7b715cb6db 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -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) diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js index 4834adeea1e..c6ad13462f9 100644 --- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js +++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js @@ -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(); + }); + }); }); diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js index ad73a67d641..8f3d1f9bf92 100644 --- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js @@ -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() { diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 8770640e769..4348a57d055 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -597,6 +597,7 @@ RSpec.describe SearchHelper do ' test' | ' test' '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 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. Don...' 'some image' | 'some image' + '

Additional information test:

' | 'Additional information test:' end with_them do diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb index 31cece108bf..8e962c0252e 100644 --- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -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 diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb index 2f64aef4fb7..1b8214ebcbb 100644 --- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -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(%()).children[0] + end + + it_behaves_like 'no project N+1 queries' + end end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 7de78710d34..c180a42c91e 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -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 } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 241ad75dcf8..d4d3aace204 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -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 diff --git a/spec/rubocop/cop/rake/require_spec.rb b/spec/rubocop/cop/rake/require_spec.rb new file mode 100644 index 00000000000..bb8c6a1f063 --- /dev/null +++ b/spec/rubocop/cop/rake/require_spec.rb @@ -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