diff options
95 files changed, 1150 insertions, 807 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5797c04c518..b98ca747c8f 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0cea2923073bcd867dd8e718a0a7b4f7de5b6094 +d72e713bd81e86d1e90cc2af435f6d3886abf6fd diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index ebeef2f2d61..5edffce6d57 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.38.0 +1.39.0 diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql index 80a37c9943d..3218c06357c 100644 --- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql +++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql @@ -2,7 +2,7 @@ query getGroupProjects($fullPath: ID!, $search: String, $after: String) { group(fullPath: $fullPath) { - projects(search: $search, after: $after, first: 100) { + projects(search: $search, after: $after, first: 100, includeSubgroups: true) { nodes { id name diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue index f06533eb9e9..ef31106b709 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue @@ -195,7 +195,6 @@ export default { <div class="alert-assignees gl-py-5 gl-w-70p" :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': !sidebarCollapsed }" - style="width: 70%" > <template v-if="sidebarCollapsed"> <div diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue index feb023fb3d5..35e2518b989 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue @@ -68,7 +68,6 @@ export default { <div class="alert-status gl-py-5 gl-w-70p" :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': !sidebarCollapsed }" - style="width: 70%" > <template v-if="sidebarCollapsed"> <div ref="status" class="gl-ml-6" data-testid="status-icon" @click="$emit('toggle-sidebar')"> diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index 60ff0a12d0c..4f0351f2003 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -141,8 +141,8 @@ module WikiActions # rubocop:disable Gitlab/ModuleWithInstanceVariables def history if page - @page_versions = Kaminari.paginate_array(page.versions(page: params[:page].to_i), - total_count: page.count_versions) + @commits = Kaminari.paginate_array(page.versions(page: params[:page].to_i), + total_count: page.count_versions) .page(params[:page]) render 'shared/wikis/history' diff --git a/app/finders/packages/group_or_project_package_finder.rb b/app/finders/packages/group_or_project_package_finder.rb new file mode 100644 index 00000000000..fb8bcfc7d42 --- /dev/null +++ b/app/finders/packages/group_or_project_package_finder.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Packages + class GroupOrProjectPackageFinder + include ::Packages::FinderHelper + + def initialize(current_user, project_or_group, params = {}) + @current_user = current_user + @project_or_group = project_or_group + @params = params + end + + def execute + raise NotImplementedError + end + + def execute! + raise NotImplementedError + end + + private + + def packages + raise NotImplementedError + end + + def base + if project? + packages_for_project(@project_or_group) + elsif group? + packages_visible_to_user(@current_user, within_group: @project_or_group) + else + ::Packages::Package.none + end + end + + def project? + @project_or_group.is_a?(::Project) + end + + def group? + @project_or_group.is_a?(::Group) + end + end +end diff --git a/app/finders/packages/maven/package_finder.rb b/app/finders/packages/maven/package_finder.rb index fd5444684c6..cc28d951f52 100644 --- a/app/finders/packages/maven/package_finder.rb +++ b/app/finders/packages/maven/package_finder.rb @@ -2,41 +2,20 @@ module Packages module Maven - class PackageFinder - include ::Packages::FinderHelper - include Gitlab::Utils::StrongMemoize - - def initialize(path, current_user, project: nil, group: nil, order_by_package_file: false) - @path = path - @current_user = current_user - @project = project - @group = group - @order_by_package_file = order_by_package_file - end - + class PackageFinder < ::Packages::GroupOrProjectPackageFinder def execute - packages_with_path.last + packages.last end def execute! - packages_with_path.last! + packages.last! end private - def base - if @project - packages_for_project(@project) - elsif @group - packages_visible_to_user(@current_user, within_group: @group) - else - ::Packages::Package.none - end - end - - def packages_with_path - matching_packages = base.only_maven_packages_with_path(@path, use_cte: @group.present?) - matching_packages = matching_packages.order_by_package_file if @order_by_package_file + def packages + matching_packages = base.only_maven_packages_with_path(@params[:path], use_cte: group?) + matching_packages = matching_packages.order_by_package_file if @params[:order_by_package_file] matching_packages end diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb index d91ef853a1a..f5eb60f2931 100644 --- a/app/finders/packages/nuget/package_finder.rb +++ b/app/finders/packages/nuget/package_finder.rb @@ -2,51 +2,23 @@ module Packages module Nuget - class PackageFinder - include ::Packages::FinderHelper - + class PackageFinder < ::Packages::GroupOrProjectPackageFinder MAX_PACKAGES_COUNT = 300 - def initialize(current_user, project_or_group, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT) - @current_user = current_user - @project_or_group = project_or_group - @package_name = package_name - @package_version = package_version - @limit = limit - end - def execute - packages.limit_recent(@limit) + packages.limit_recent(@params[:limit] || MAX_PACKAGES_COUNT) end private - def base - if project? - packages_for_project(@project_or_group) - elsif group? - packages_visible_to_user(@current_user, within_group: @project_or_group) - else - ::Packages::Package.none - end - end - def packages result = base.nuget .has_version .processed - .with_name_like(@package_name) - result = result.with_version(@package_version) if @package_version.present? + .with_name_like(@params[:package_name]) + result = result.with_version(@params[:package_version]) if @params[:package_version].present? result end - - def project? - @project_or_group.is_a?(::Project) - end - - def group? - @project_or_group.is_a?(::Group) - end end end end diff --git a/app/finders/packages/pypi/package_finder.rb b/app/finders/packages/pypi/package_finder.rb new file mode 100644 index 00000000000..3bb484b34f2 --- /dev/null +++ b/app/finders/packages/pypi/package_finder.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Packages + module Pypi + class PackageFinder < ::Packages::GroupOrProjectPackageFinder + def execute + packages.by_file_name_and_sha256(@params[:filename], @params[:sha256]) + end + + private + + def packages + base.pypi.has_version.processed + end + end + end +end diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb new file mode 100644 index 00000000000..223caecb4dc --- /dev/null +++ b/app/finders/packages/pypi/packages_finder.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Packages + module Pypi + class PackagesFinder < ::Packages::GroupOrProjectPackageFinder + def execute! + results = packages.with_normalized_pypi_name(@params[:package_name]) + raise ActiveRecord::RecordNotFound if results.empty? + + results + end + + private + + def packages + base.pypi.has_version.processed + end + end + end +end diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql index 959bf7dc91d..873ecc81466 100644 --- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql +++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql @@ -27,6 +27,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { __typename id iid + complete usesNeeds downstream { __typename diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb index 619448cbc18..08b29d884b0 100644 --- a/app/graphql/resolvers/design_management/versions_resolver.rb +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -62,6 +62,7 @@ module Resolvers ::DesignManagement::VersionsFinder .new(design_or_collection, current_user, params) .execute + .with_author end def by_id(gid) diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 0979a71e07d..2eeddaca6ba 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -24,6 +24,9 @@ module Types field :before_sha, GraphQL::STRING_TYPE, null: true, description: 'Base SHA of the source branch.' + field :complete, GraphQL::BOOLEAN_TYPE, null: false, method: :complete?, + description: 'Indicates if a pipeline is complete.' + field :status, PipelineStatusEnum, null: false, description: "Status of the pipeline (#{::Ci::Pipeline.all_state_names.compact.join(', ').upcase})" diff --git a/app/graphql/types/design_management/version_type.rb b/app/graphql/types/design_management/version_type.rb index 4bc71aef0f4..265d6185110 100644 --- a/app/graphql/types/design_management/version_type.rb +++ b/app/graphql/types/design_management/version_type.rb @@ -32,6 +32,10 @@ module Types null: false, description: 'A particular design as of this version, provided it is visible at this version.', resolver: ::Resolvers::DesignManagement::Version::DesignsAtVersionResolver.single + + field :author, Types::UserType, null: false, description: 'Author of the version.' + field :created_at, Types::TimeType, null: false, + description: 'Timestamp of when the version was created.' end end end diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 90b8a8e94b0..0d27e07f172 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -1,12 +1,30 @@ # frozen_string_literal: true module WebpackHelper + def prefetch_link_tag(source) + href = asset_path(source) + + link_tag = tag.link(rel: 'prefetch', href: href) + + early_hints_link = "<#{href}>; rel=prefetch" + + request.send_early_hints("Link" => early_hints_link) + + link_tag + end + def webpack_bundle_tag(bundle) javascript_include_tag(*webpack_entrypoint_paths(bundle)) end def webpack_preload_asset_tag(asset, options = {}) - preload_link_tag(Gitlab::Webpack::Manifest.asset_paths(asset).first, options) + path = Gitlab::Webpack::Manifest.asset_paths(asset).first + + if options.delete(:prefetch) + prefetch_link_tag(path) + else + preload_link_tag(path, options) + end end def webpack_controller_bundle_tags diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index ffd3864c18b..5248a80f710 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -261,6 +261,22 @@ module Ci self.where(project: project).sum(:size) end + ## + # FastDestroyAll concerns + # rubocop: disable CodeReuse/ServiceClass + def self.begin_fast_destroy + service = ::Ci::JobArtifacts::DestroyAssociationsService.new(self) + service.destroy_records + service + end + # rubocop: enable CodeReuse/ServiceClass + + ## + # FastDestroyAll concerns + def self.finalize_fast_destroy(service) + service.update_statistics + end + def local_store? [nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2e837c8acb1..78e6f59c02c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -17,6 +17,7 @@ module Ci include FromUnion include UpdatedAtFilterable include EachBatch + include FastDestroyAll::Helpers MAX_OPEN_MERGE_REQUESTS_REFS = 4 @@ -126,6 +127,8 @@ module Ci after_create :keep_around_commits, unless: :importing? + use_fast_destroy :job_artifacts + # We use `Enums::Ci::Pipeline.sources` here so that EE can more easily extend # this `Hash` with new values. enum_with_nil source: Enums::Ci::Pipeline.sources diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb index 68a0b8faf4f..ca65cf38f0d 100644 --- a/app/models/design_management/version.rb +++ b/app/models/design_management/version.rb @@ -58,6 +58,7 @@ module DesignManagement scope :ordered, -> { order(id: :desc) } scope :for_issue, -> (issue) { where(issue: issue) } scope :by_sha, -> (sha) { where(sha: sha) } + scope :with_author, -> { includes(:author) } # This is the one true way to create a Version. # diff --git a/app/models/project.rb b/app/models/project.rb index 19298f28f53..9d8bd2dbb36 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2578,6 +2578,12 @@ class Project < ApplicationRecord Gitlab::Routing.url_helpers.activity_project_path(self) end + def increment_statistic_value(statistic, delta) + return if pending_delete? + + ProjectStatistics.increment_statistic(self, statistic, delta) + end + private def set_container_registry_access_level diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 3b9a7ded83e..9ae5a870323 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -127,10 +127,21 @@ class WikiPage @path ||= @page.path end + # Returns a CommitCollection + # + # Queries the commits for current page's path, equivalent to + # `git log path/to/page`. Filters and options supported: + # https://gitlab.com/gitlab-org/gitaly/-/blob/master/proto/commit.proto#L322-344 def versions(options = {}) return [] unless persisted? - wiki.wiki.page_versions(page.path, options) + default_per_page = Kaminari.config.default_per_page + offset = [options[:page].to_i - 1, 0].max * options.fetch(:per_page, default_per_page) + + wiki.repository.commits('HEAD', + path: page.path, + limit: options.fetch(:limit, default_per_page), + offset: offset) end def count_versions diff --git a/app/policies/concerns/readonly_abilities.rb b/app/policies/concerns/readonly_abilities.rb index 723bbe37769..300f17088b7 100644 --- a/app/policies/concerns/readonly_abilities.rb +++ b/app/policies/concerns/readonly_abilities.rb @@ -13,6 +13,7 @@ module ReadonlyAbilities create_merge_request_from create_merge_request_in award_emoji + create_incident ].freeze READONLY_FEATURES = %i[ diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 5b4160c211a..943aaee5817 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -226,6 +226,8 @@ class ProjectPolicy < BasePolicy enable :read_insights end + rule { can?(:guest_access) & can?(:create_issue) }.enable :create_incident + # These abilities are not allowed to admins that are not members of the project, # that's why they are defined separately. rule { guest & can?(:download_code) }.enable :build_download_code diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index d05b500b140..eb8622edb38 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -7,6 +7,7 @@ class JobEntity < Grape::Entity expose :name expose :started?, as: :started + expose :complete?, as: :complete expose :archived?, as: :archived # bridge jobs don't have build detail pages diff --git a/app/services/ci/job_artifacts/destroy_associations_service.rb b/app/services/ci/job_artifacts/destroy_associations_service.rb new file mode 100644 index 00000000000..794d24eadf2 --- /dev/null +++ b/app/services/ci/job_artifacts/destroy_associations_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Ci + module JobArtifacts + class DestroyAssociationsService + BATCH_SIZE = 100 + + def initialize(job_artifacts_relation) + @job_artifacts_relation = job_artifacts_relation + @statistics = {} + end + + def destroy_records + @job_artifacts_relation.each_batch(of: BATCH_SIZE) do |relation| + service = Ci::JobArtifacts::DestroyBatchService.new(relation, pick_up_at: Time.current) + result = service.execute(update_stats: false) + updates = result[:statistics_updates] + + @statistics.merge!(updates) { |_key, oldval, newval| newval + oldval } + end + end + + def update_statistics + @statistics.each do |project, delta| + project.increment_statistic_value(Ci::JobArtifact.project_statistics_name, delta) + end + end + end + end +end diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb index 8ecf5cf55dd..8536b88ccc0 100644 --- a/app/services/ci/job_artifacts/destroy_batch_service.rb +++ b/app/services/ci/job_artifacts/destroy_batch_service.rb @@ -23,8 +23,8 @@ module Ci end # rubocop: disable CodeReuse/ActiveRecord - def execute - return success(destroyed_artifacts_count: artifacts_count) if @job_artifacts.empty? + def execute(update_stats: true) + return success(destroyed_artifacts_count: 0, statistics_updates: {}) if @job_artifacts.empty? Ci::DeletedObject.transaction do Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at) @@ -33,10 +33,11 @@ module Ci end # This is executed outside of the transaction because it depends on Redis - update_project_statistics + update_project_statistics! if update_stats increment_monitoring_statistics(artifacts_count) - success(destroyed_artifacts_count: artifacts_count) + success(destroyed_artifacts_count: artifacts_count, + statistics_updates: affected_project_statistics) end # rubocop: enable CodeReuse/ActiveRecord @@ -45,12 +46,20 @@ module Ci # This method is implemented in EE and it must do only database work def destroy_related_records(artifacts); end - def update_project_statistics - artifacts_by_project = @job_artifacts.group_by(&:project) - artifacts_by_project.each do |project, artifacts| - delta = -artifacts.sum { |artifact| artifact.size.to_i } - ProjectStatistics.increment_statistic( - project, Ci::JobArtifact.project_statistics_name, delta) + # using ! here since this can't be called inside a transaction + def update_project_statistics! + affected_project_statistics.each do |project, delta| + project.increment_statistic_value(Ci::JobArtifact.project_statistics_name, delta) + end + end + + def affected_project_statistics + strong_memoize(:affected_project_statistics) do + artifacts_by_project = @job_artifacts.group_by(&:project) + artifacts_by_project.each.with_object({}) do |(project, artifacts), accumulator| + delta = -artifacts.sum { |artifact| artifact.size.to_i } + accumulator[project] = delta + end end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 54b9e9d6c4b..72e906e20f1 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -37,6 +37,8 @@ module Issues def filter_params(issue) super + params.delete(:issue_type) unless issue_type_allowed?(issue) + moved_issue = params.delete(:moved_issue) # Setting created_at, updated_at and iid is allowed only for admins and owners or @@ -75,6 +77,11 @@ module Issues Milestones::IssuesCountService.new(milestone).delete_cache end + + # @param object [Issue, Project] + def issue_type_allowed?(object) + can?(current_user, :"create_#{params[:issue_type]}", object) + end end end diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index bdb4b39b0ba..5cb138946d7 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -64,20 +64,17 @@ module Issues private - def allowed_issue_base_params - [:title, :description, :confidential, :issue_type] - end + def allowed_issue_params + allowed_params = [ + :title, + :description, + :confidential + ] - def allowed_issue_admin_params - [:milestone_id] - end + allowed_params << :milestone_id if can?(current_user, :admin_issue, project) + allowed_params << :issue_type if issue_type_allowed?(project) - def allowed_issue_params - if can?(current_user, :admin_issue, project) - params.slice(*(allowed_issue_base_params + allowed_issue_admin_params)) - else - params.slice(*allowed_issue_base_params) - end + params.slice(*allowed_params) end def build_issue_params diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index d9371e331c1..19365374e10 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -28,7 +28,6 @@ module Issues # because we do allow users that cannot admin issues to set confidential flag when creating an issue unless can_admin_issuable?(issue) params.delete(:confidential) - params.delete(:issue_type) end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index a6fd9843b2a..112d156c214 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -6,7 +6,7 @@ module Labels @current_user = current_user @parent = parent @available_labels = params.delete(:available_labels) - @existing_labels_by_title = params.delete(:existing_labels_by_title) || {} + @existing_labels_by_title = params.delete(:existing_labels_by_title) @params = params.dup.with_indifferent_access end @@ -45,7 +45,9 @@ module Labels # rubocop: disable CodeReuse/ActiveRecord def find_existing_label(title) - existing_labels_by_title[title] || available_labels.find_by(title: title) + return existing_labels_by_title[title] if existing_labels_by_title + + available_labels.find_by(title: title) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb index 420afac7de3..f99db35fd49 100644 --- a/app/services/merge_requests/update_assignees_service.rb +++ b/app/services/merge_requests/update_assignees_service.rb @@ -45,7 +45,7 @@ module MergeRequests end def assignee_ids - params.fetch(:assignee_ids).first(1) + params.fetch(:assignee_ids).reject { _1 == 0 }.first(1) end def params diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb index a6cffa3038c..c7ffd468864 100644 --- a/app/services/packages/maven/find_or_create_package_service.rb +++ b/app/services/packages/maven/find_or_create_package_service.rb @@ -6,7 +6,7 @@ module Packages def execute package = - ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project) + ::Packages::Maven::PackageFinder.new(current_user, project, path: params[:path]) .execute unless Namespace::PackageSetting.duplicates_allowed?(package) diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml index 4fc00dfb3f4..89c029621e7 100644 --- a/app/views/ide/_show.html.haml +++ b/app/views/ide/_show.html.haml @@ -4,6 +4,9 @@ - add_page_specific_style 'page_bundles/build' - add_page_specific_style 'page_bundles/ide' +- content_for :monaco_tag do + - webpack_preload_asset_tag('monaco') + #ide.ide-loading{ data: ide_data } .text-center .gl-spinner.gl-spinner-md diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 8aa163a26ec..329c660f3a0 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -32,7 +32,7 @@ - if page_canonical_link %link{ rel: 'canonical', href: page_canonical_link } - = webpack_preload_asset_tag("monaco") + = yield :monaco_tag = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index abfed450316..68c24e994f6 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,5 +1,7 @@ - breadcrumb_title _("Repository") - page_title _("Edit"), @blob.path, @ref +- content_for :monaco_tag do + - webpack_preload_asset_tag('monaco') - if @conflict .gl-alert.gl-alert-danger.gl-mb-5.gl-mt-5 diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index c66300aa947..d92ecd7e037 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,6 +1,8 @@ - breadcrumb_title "Repository" - page_title @blob.path, @ref - signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1) +- content_for :monaco_tag do + - webpack_preload_asset_tag('monaco', prefetch: true) .js-signature-container{ data: { 'signatures-path': signatures_path } } diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml index eb588e150f7..ca3f671c9fb 100644 --- a/app/views/projects/ci/pipeline_editor/show.html.haml +++ b/app/views/projects/ci/pipeline_editor/show.html.haml @@ -1,3 +1,5 @@ - page_title s_('Pipelines|Pipeline Editor') +- content_for :monaco_tag do + - webpack_preload_asset_tag('monaco') #js-pipeline-editor{ data: js_pipeline_editor_data(@project) } diff --git a/app/views/shared/wikis/history.html.haml b/app/views/shared/wikis/history.html.haml index 079b9768730..afbed3b0f42 100644 --- a/app/views/shared/wikis/history.html.haml +++ b/app/views/shared/wikis/history.html.haml @@ -20,7 +20,7 @@ %th= _('Changes') %th= _('Last updated') %tbody - - @page_versions.each do |commit| + - @commits.each do |commit| %tr %td = link_to wiki_page_path(@wiki, @page, version_id: commit.id) do @@ -33,6 +33,6 @@ = commit.message %td = time_ago_with_tooltip(commit.authored_date) - = paginate @page_versions, theme: 'gitlab' + = paginate @commits, theme: 'gitlab' = render 'shared/wikis/sidebar' diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 66f5e8148e1..6e0126fd2fd 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,5 +1,7 @@ - page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") - @content_class = "limit-container-width" unless fluid_layout +- content_for :monaco_tag do + - webpack_preload_asset_tag('monaco') %h3.page-title = _("Edit Snippet") diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index beb4cf4a6aa..534bbbef437 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -9,6 +9,8 @@ - add_to_breadcrumbs _("Snippets"), dashboard_snippets_path - breadcrumb_title @snippet.to_reference - page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") +- content_for :monaco_tag do + - webpack_preload_asset_tag('monaco', prefetch: true) #js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} } diff --git a/changelogs/unreleased/194104-part-2.yml b/changelogs/unreleased/194104-part-2.yml new file mode 100644 index 00000000000..0024dbb0974 --- /dev/null +++ b/changelogs/unreleased/194104-part-2.yml @@ -0,0 +1,5 @@ +--- +title: Fix repeating SQL queries when changing labels for a resource. +merge_request: 60718 +author: +type: performance diff --git a/changelogs/unreleased/224151-remove-job-artifacts-when-removing-a-pipeline.yml b/changelogs/unreleased/224151-remove-job-artifacts-when-removing-a-pipeline.yml new file mode 100644 index 00000000000..965b09244b8 --- /dev/null +++ b/changelogs/unreleased/224151-remove-job-artifacts-when-removing-a-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Fast destroy job artifacts when destroying a pipeline +merge_request: 60391 +author: +type: fixed diff --git a/changelogs/unreleased/296547-enable-drawer-by-default.yml b/changelogs/unreleased/296547-enable-drawer-by-default.yml new file mode 100644 index 00000000000..f9fd42bf7c7 --- /dev/null +++ b/changelogs/unreleased/296547-enable-drawer-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Add pipeline editor drawer for introduction to CI +merge_request: 61620 +author: +type: added diff --git a/changelogs/unreleased/32081-design-rich-data.yml b/changelogs/unreleased/32081-design-rich-data.yml new file mode 100644 index 00000000000..2b43f097c59 --- /dev/null +++ b/changelogs/unreleased/32081-design-rich-data.yml @@ -0,0 +1,5 @@ +--- +title: Add fields to graphQL version type +merge_request: 61567 +author: +type: added diff --git a/changelogs/unreleased/330369-unassign-all-assignees-from-merge-request-via-rest-api-not-possibl.yml b/changelogs/unreleased/330369-unassign-all-assignees-from-merge-request-via-rest-api-not-possibl.yml new file mode 100644 index 00000000000..baa61911be2 --- /dev/null +++ b/changelogs/unreleased/330369-unassign-all-assignees-from-merge-request-via-rest-api-not-possibl.yml @@ -0,0 +1,5 @@ +--- +title: 'Merge Request API: Treat 0 as a non-assigning sentinel value' +merge_request: 61301 +author: +type: fixed diff --git a/changelogs/unreleased/add-complete-field-to-pipeline-and-job.yml b/changelogs/unreleased/add-complete-field-to-pipeline-and-job.yml new file mode 100644 index 00000000000..5f57eb7eecf --- /dev/null +++ b/changelogs/unreleased/add-complete-field-to-pipeline-and-job.yml @@ -0,0 +1,5 @@ +--- +title: Add complete field to indicate if a pipeline/job is complete +merge_request: 61209 +author: Cong Chen @gentcys +type: added diff --git a/changelogs/unreleased/dmishunov-monaco-tag.yml b/changelogs/unreleased/dmishunov-monaco-tag.yml new file mode 100644 index 00000000000..e09a01b607c --- /dev/null +++ b/changelogs/unreleased/dmishunov-monaco-tag.yml @@ -0,0 +1,5 @@ +--- +title: Introduced granular control to Monaco tag +merge_request: 61690 +author: +type: performance diff --git a/changelogs/unreleased/sy-restrict-issue-creation-to-appropriate-users.yml b/changelogs/unreleased/sy-restrict-issue-creation-to-appropriate-users.yml new file mode 100644 index 00000000000..ff3f8525e14 --- /dev/null +++ b/changelogs/unreleased/sy-restrict-issue-creation-to-appropriate-users.yml @@ -0,0 +1,5 @@ +--- +title: Restrict issue creation via API by relevant permissions +merge_request: 61281 +author: +type: fixed diff --git a/changelogs/unreleased/upgrade-pages-to-1-39-0.yml b/changelogs/unreleased/upgrade-pages-to-1-39-0.yml new file mode 100644 index 00000000000..fb3c0601bae --- /dev/null +++ b/changelogs/unreleased/upgrade-pages-to-1-39-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Pages to v1.39.0 +merge_request: 61756 +author: +type: added diff --git a/changelogs/unreleased/zj-port-page-versions-wiki.yml b/changelogs/unreleased/zj-port-page-versions-wiki.yml new file mode 100644 index 00000000000..b4bf09079e8 --- /dev/null +++ b/changelogs/unreleased/zj-port-page-versions-wiki.yml @@ -0,0 +1,5 @@ +--- +title: 'Wiki: Use FindAllCommits RPC to list page versions' +merge_request: 61459 +author: +type: changed diff --git a/config/feature_flags/development/pipeline_editor_drawer.yml b/config/feature_flags/development/pipeline_editor_drawer.yml index 354161b0ae8..df73c4be01e 100644 --- a/config/feature_flags/development/pipeline_editor_drawer.yml +++ b/config/feature_flags/development/pipeline_editor_drawer.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329806 milestone: '13.12' type: development group: group::pipeline authoring -default_enabled: false +default_enabled: true diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index cd9192f9556..4acc051e3c4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -8024,6 +8024,8 @@ A specific version in which designs were added, modified or deleted. | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="designversionauthor"></a>`author` | [`UserCore!`](#usercore) | Author of the version. | +| <a id="designversioncreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the version was created. | | <a id="designversiondesigns"></a>`designs` | [`DesignConnection!`](#designconnection) | All designs that were changed in the version. (see [Connections](#connections)) | | <a id="designversionid"></a>`id` | [`ID!`](#id) | ID of the design version. | | <a id="designversionsha"></a>`sha` | [`ID!`](#id) | SHA of the design version. | @@ -10623,6 +10625,7 @@ Information about pagination in a connection. | <a id="pipelinecodequalityreports"></a>`codeQualityReports` | [`CodeQualityDegradationConnection`](#codequalitydegradationconnection) | Code Quality degradations reported on the pipeline. (see [Connections](#connections)) | | <a id="pipelinecommitpath"></a>`commitPath` | [`String`](#string) | Path to the commit that triggered the pipeline. | | <a id="pipelinecommittedat"></a>`committedAt` | [`Time`](#time) | Timestamp of the pipeline's commit. | +| <a id="pipelinecomplete"></a>`complete` | [`Boolean!`](#boolean) | Indicates if a pipeline is complete. | | <a id="pipelineconfigsource"></a>`configSource` | [`PipelineConfigSourceEnum`](#pipelineconfigsourceenum) | Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE). | | <a id="pipelinecoverage"></a>`coverage` | [`Float`](#float) | Coverage percentage. | | <a id="pipelinecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of the pipeline's creation. | diff --git a/doc/user/group/bulk_editing/img/bulk_editing_v13_11.png b/doc/user/group/bulk_editing/img/bulk_editing_v13_11.png Binary files differdeleted file mode 100644 index 4db2bbf264f..00000000000 --- a/doc/user/group/bulk_editing/img/bulk_editing_v13_11.png +++ /dev/null diff --git a/doc/user/group/bulk_editing/index.md b/doc/user/group/bulk_editing/index.md index 2a3e010282f..48644b7427d 100644 --- a/doc/user/group/bulk_editing/index.md +++ b/doc/user/group/bulk_editing/index.md @@ -1,79 +1,8 @@ --- -stage: Plan -group: Project Management -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +redirect_to: '../../../user/group/index.md' --- -# Bulk editing issues, epics, and merge requests at the group level **(PREMIUM)** +This document was moved to [another location](../../../user/group/index.md). -NOTE: -Bulk editing issues and merge requests is also available at the **project level**. -For more details, see [Bulk editing issues and merge requests at the project level](../../project/bulk_editing.md). - -If you want to update attributes across multiple issues, epics, or merge requests in a group, you -can do it by bulk editing them, that is, editing them together. - -Only the items visible on the current page are selected for bulk editing (up to 20). - -![Bulk editing](img/bulk_editing_v13_11.png) - -## Bulk edit issues at the group level - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7249) in GitLab 12.1. -> - Assigning epic ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2. -> - Editing health status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218395) in GitLab 13.2. -> - Editing iteration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/196806) in GitLab 13.9. - -Users with permission level of [Reporter or higher](../../permissions.md) can manage issues. - -When bulk editing issues in a group, you can edit the following attributes: - -- [Epic](../epics/index.md) -- [Milestone](../../project/milestones/index.md) -- [Labels](../../project/labels.md) -- [Health status](../../project/issues/managing_issues.md#health-status) -- [Iteration](../iterations/index.md) - -To update multiple project issues at the same time: - -1. In a group, go to **{issues}** **Issues > List**. -1. Click **Edit issues**. A sidebar on the right-hand side of your screen appears with editable fields. -1. Select the checkboxes next to each issue you want to edit. -1. Select the appropriate fields and their values from the sidebar. -1. Click **Update all**. - -## Bulk edit epics - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7250) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2. - -Users with permission level of [Reporter or higher](../../permissions.md) can manage epics. - -When bulk editing epics in a group, you can edit their labels. - -To update multiple epics at the same time: - -1. In a group, go to **{epic}** **Epics > List**. -1. Click **Edit epics**. A sidebar on the right-hand side of your screen appears with editable fields. -1. Check the checkboxes next to each epic you want to edit. -1. Select the appropriate fields and their values from the sidebar. -1. Click **Update all**. - -## Bulk edit merge requests at the group level - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12719) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2. - -Users with permission level of [Developer or higher](../../permissions.md) can manage merge requests. - -When bulk editing merge requests in a group, you can edit the following attributes: - -- Milestone -- Labels - -To update multiple group merge requests at the same time: - -1. In a group, go to **{merge-request}** **Merge Requests**. -1. Click **Edit merge requests**. A sidebar on the right-hand side of your screen appears with - editable fields. -1. Select the checkboxes next to each merge request you want to edit. -1. Select the appropriate fields and their values from the sidebar. -1. Click **Update all**. +<!-- This redirect file can be deleted after <2021-08-13>. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md index 050ff4c6566..206cd5246f1 100644 --- a/doc/user/group/epics/index.md +++ b/doc/user/group/epics/index.md @@ -43,7 +43,7 @@ To learn what you can do with an epic, see [Manage epics](manage_epics.md). Poss - [Create an epic](manage_epics.md#create-an-epic) - [Edit an epic](manage_epics.md#edit-an-epic) -- [Bulk-edit epics](../bulk_editing/index.md#bulk-edit-epics) +- [Bulk-edit epics](manage_epics.md#bulk-edit-epics) - [Delete an epic](manage_epics.md#delete-an-epic) - [Close an epic](manage_epics.md#close-an-epic) - [Reopen a closed epic](manage_epics.md#reopen-a-closed-epic) diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index d24aea60344..03ff9b9b165 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -60,10 +60,21 @@ To edit an epics' start date, due date, or labels: 1. Select **Edit** next to each section in the epic sidebar. 1. Select the dates or labels for your epic. -## Bulk-edit epics +## Bulk edit epics -You can edit multiple epics at once. To learn how to do it, visit -[Bulk editing issues, epics, and merge requests at the group level](../bulk_editing/index.md#bulk-edit-epics). +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7250) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2. + +Users with permission level of [Reporter or higher](../../permissions.md) can manage epics. + +When bulk editing epics in a group, you can edit their labels. + +To update multiple epics at the same time: + +1. In a group, go to **Epics > List**. +1. Click **Edit epics**. A sidebar on the right-hand side of your screen appears with editable fields. +1. Check the checkboxes next to each epic you want to edit. +1. Select the appropriate fields and their values from the sidebar. +1. Click **Update all**. ## Delete an epic diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index f301ec5cd34..81166e2dfbd 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -169,6 +169,31 @@ To update multiple project issues at the same time: 1. Select the appropriate fields and their values from the sidebar. 1. Click **Update all**. +## Bulk edit issues at the group level + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7249) in GitLab 12.1. +> - Assigning epic ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2. +> - Editing health status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218395) in GitLab 13.2. +> - Editing iteration [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/196806) in GitLab 13.9. + +Users with permission level of [Reporter or higher](../../permissions.md) can manage issues. + +When bulk editing issues in a group, you can edit the following attributes: + +- [Epic](../../group/epics/index.md) +- [Milestone](../milestones/index.md) +- [Labels](../labels.md) +- [Health status](#health-status) +- [Iteration](../../group/iterations/index.md) + +To update multiple project issues at the same time: + +1. In a group, go to **Issues > List**. +1. Click **Edit issues**. A sidebar on the right-hand side of your screen appears with editable fields. +1. Select the checkboxes next to each issue you want to edit. +1. Select the appropriate fields and their values from the sidebar. +1. Click **Update all**. + ## Moving issues Moving an issue copies it to the target project, and closes it in the originating project. diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md index 76913351283..b33919c7fbe 100644 --- a/doc/user/project/merge_requests/browser_performance_testing.md +++ b/doc/user/project/merge_requests/browser_performance_testing.md @@ -64,7 +64,7 @@ using Docker-in-Docker. 1. First, set up GitLab Runner with a [Docker-in-Docker build](../../../ci/docker/using_docker_build.md#use-the-docker-executor-with-the-docker-image-docker-in-docker). -1. Configure the default Browser Performance Testing CI job as follows in your `.gitlab-ci.yml` file: +1. Configure the default Browser Performance Testing CI/CD job as follows in your `.gitlab-ci.yml` file: ```yaml include: @@ -75,17 +75,19 @@ using Docker-in-Docker. URL: https://example.com ``` -NOTE: -For versions before 12.4, see the information for [older GitLab versions](#gitlab-versions-123-and-older). -If you are using a Kubernetes cluster, use [`template: Jobs/Browser-Performance-Testing.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml) -instead. +WARNING: +In GitLab 14.0 and later, the job [is scheduled to be renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) +from `performance` to `browser_performance`. -The above example creates a `performance` job in your CI/CD pipeline and runs -sitespeed.io against the webpage you defined in `URL` to gather key metrics. +The above example: -The example uses a CI/CD template that is included in all GitLab installations since -12.4, but it doesn't work with Kubernetes clusters. If you are using GitLab 12.3 -or older, you must [add the configuration manually](#gitlab-versions-123-and-older) +- Creates a `performance` job in your CI/CD pipeline and runs sitespeed.io against the webpage you + defined in `URL` to gather key metrics. +- Uses a template that doesn't work with Kubernetes clusters. If you are using a Kubernetes cluster, + use [`template: Jobs/Browser-Performance-Testing.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml) + instead. +- Uses a CI/CD template that is included in all GitLab installations since 12.4. If you are using + GitLab 12.3 or earlier, you must [add the configuration manually](#gitlab-versions-132-and-earlier). The template uses the [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance), and it saves the full HTML sitespeed.io report as a [Browser Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance) @@ -181,63 +183,62 @@ performance: URL: environment_url.txt ``` -### GitLab versions 12.3 and older +### GitLab versions 13.2 and earlier -Browser Performance Testing has gone through several changes since it's introduction. +Browser Performance Testing has gone through several changes since its introduction. In this section we detail these changes and how you can run the test based on your GitLab version: +- In 13.2 the feature was renamed from `Performance` to `Browser Performance` with additional + template CI/CD variables. - In GitLab 12.4 [a job template was made available](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml). -- In 13.2 the feature was renamed from `Performance` to `Browser Performance` with -additional template CI/CD variables. The job name in the template is still `performance` -for compatibility reasons, but may be renamed to match in a future iteration. - For 11.5 to 12.3 no template is available and the job has to be defined manually as follows: -```yaml -performance: - stage: performance - image: docker:git - variables: - URL: https://example.com - SITESPEED_VERSION: 14.1.0 - SITESPEED_OPTIONS: '' - services: - - docker:stable-dind - script: - - mkdir gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js - - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS - - mv sitespeed-results/data/performance.json performance.json - artifacts: - paths: - - performance.json - - sitespeed-results/ - reports: - performance: performance.json -``` + ```yaml + performance: + stage: performance + image: docker:git + variables: + URL: https://example.com + SITESPEED_VERSION: 14.1.0 + SITESPEED_OPTIONS: '' + services: + - docker:stable-dind + script: + - mkdir gitlab-exporter + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js + - mkdir sitespeed-results + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - performance.json + - sitespeed-results/ + reports: + performance: performance.json + ``` - For 11.4 and earlier the job should be defined as follows: -```yaml -performance: - stage: performance - image: docker:git - variables: - URL: https://example.com - services: - - docker:stable-dind - script: - - mkdir gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js - - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL - - mv sitespeed-results/data/performance.json performance.json - artifacts: - paths: - - performance.json - - sitespeed-results/ -``` + ```yaml + performance: + stage: performance + image: docker:git + variables: + URL: https://example.com + services: + - docker:stable-dind + script: + - mkdir gitlab-exporter + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js + - mkdir sitespeed-results + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - performance.json + - sitespeed-results/ + ``` Upgrading to the latest version and using the templates is recommended, to ensure you receive the latest updates, including updates to the sitespeed.io versions. diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md index 36369e92910..e98a230c0de 100644 --- a/doc/user/project/merge_requests/reviews/index.md +++ b/doc/user/project/merge_requests/reviews/index.md @@ -52,6 +52,26 @@ To update multiple project merge requests at the same time: 1. Select the appropriate fields and their values from the sidebar. 1. Click **Update all**. +## Bulk edit merge requests at the group level + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12719) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2. + +Users with permission level of [Developer or higher](../../../permissions.md) can manage merge requests. + +When bulk editing merge requests in a group, you can edit the following attributes: + +- Milestone +- Labels + +To update multiple group merge requests at the same time: + +1. In a group, go to **Merge requests**. +1. Click **Edit merge requests**. A sidebar on the right-hand side of your screen appears with + editable fields. +1. Select the checkboxes next to each merge request you want to edit. +1. Select the appropriate fields and their values from the sidebar. +1. Click **Update all**. + ## Review a merge request > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4213) in GitLab Premium 11.4. diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index 2790e5cfffb..22f7b07809b 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -92,10 +92,9 @@ module API !params[:path].include?(::Packages::Maven::FindOrCreatePackageService::SNAPSHOT_TERM) ::Packages::Maven::PackageFinder.new( - params[:path], current_user, - project: project, - group: group, + project || group, + path: params[:path], order_by_package_file: order_by_package_file ).execute! end diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 658c6d13847..73b2f658825 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -24,25 +24,6 @@ module API render_api_error!(e.message, 400) end - helpers do - def packages_finder(project = authorized_user_project) - project - .packages - .pypi - .has_version - .processed - end - - def find_package_versions - packages = packages_finder - .with_normalized_pypi_name(params[:package_name]) - - not_found!('Package') if packages.empty? - - packages - end - end - before do require_packages_enabled! end @@ -71,7 +52,7 @@ module API project = unauthorized_user_project! filename = "#{params[:file_identifier]}.#{params[:format]}" - package = packages_finder(project).by_file_name_and_sha256(filename, params[:sha256]) + package = Packages::Pypi::PackageFinder.new(current_user, project, { filename: filename, sha256: params[:sha256] }).execute package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: false).execute track_package_event('pull_package', :pypi) @@ -95,7 +76,7 @@ module API track_package_event('list_package', :pypi) - packages = find_package_versions + packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute! presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) # Adjusts grape output format diff --git a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb index f3fc87cbac7..1c0a83285a6 100644 --- a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb +++ b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb @@ -22,7 +22,16 @@ module Gitlab .where("traversal_ids = '{}'") ranged_query.each_batch(of: sub_batch_size) do |sub_batch| - sub_batch.update_all('traversal_ids = ARRAY[id]') + first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + + # The query need to be reconstructed because .each_batch modifies the default scope + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510 + Namespace.unscoped + .base_query + .where(id: first..last) + .where("traversal_ids = '{}'") + .update_all('traversal_ids = ARRAY[id]') + sleep PAUSE_SECONDS end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 4e4e246419e..5616b61de07 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -96,22 +96,6 @@ module Gitlab end end - # options: - # :page - The Integer page number. - # :per_page - The number of items per page. - # :limit - Total number of items to return. - def page_versions(page_path, options = {}) - versions = wrapped_gitaly_errors do - gitaly_wiki_client.page_versions(page_path, options) - end - - # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20 - # per page, but also fetches 20 if `limit` or `per_page` < 20. - # Slicing returns an array with the expected number of items. - slice_bound = options[:limit] || options[:per_page] || DEFAULT_PAGINATION - versions[0..slice_bound] - end - def count_page_versions(page_path) @repository.count_commits(ref: 'HEAD', path: page_path) end diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 78283a67265..3613cd01122 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -119,30 +119,6 @@ module Gitlab pages end - # options: - # :page - The Integer page number. - # :per_page - The number of items per page. - # :limit - Total number of items to return. - def page_versions(page_path, options) - request = Gitaly::WikiGetPageVersionsRequest.new( - repository: @gitaly_repo, - page_path: encode_binary(page_path), - page: options[:page] || 1, - per_page: options[:per_page] || Gitlab::Git::Wiki::DEFAULT_PAGINATION - ) - - stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_page_versions, request, timeout: GitalyClient.medium_timeout) - - versions = [] - stream.each do |message| - message.versions.each do |version| - versions << new_wiki_page_version(version) - end - end - - versions - end - private # If a block is given and the yielded value is truthy, iteration will be diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 165a6ed4002..8087d3c3c32 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -860,9 +860,6 @@ msgstr "" msgid "%{strongStart}Deletes%{strongEnd} source branch" msgstr "" -msgid "%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position." -msgstr "" - msgid "%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}" msgstr "" @@ -9774,24 +9771,6 @@ msgstr "" msgid "Custom range (UTC)" msgstr "" -msgid "CustomCycleAnalytics|Add a stage" -msgstr "" - -msgid "CustomCycleAnalytics|Add stage" -msgstr "" - -msgid "CustomCycleAnalytics|Editing stage" -msgstr "" - -msgid "CustomCycleAnalytics|End event label" -msgstr "" - -msgid "CustomCycleAnalytics|Stage name already exists" -msgstr "" - -msgid "CustomCycleAnalytics|Start event label" -msgstr "" - msgid "Customer Portal" msgstr "" @@ -11902,9 +11881,6 @@ msgstr "" msgid "Edit sidebar" msgstr "" -msgid "Edit stage" -msgstr "" - msgid "Edit this file only." msgstr "" @@ -31871,9 +31847,6 @@ msgstr "" msgid "Test Cases" msgstr "" -msgid "Test cases are not available for this project" -msgstr "" - msgid "Test coverage parsing" msgstr "" diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 20ae569322c..129d03d17f3 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -10,6 +10,9 @@ RSpec.describe 'Issue Boards new issue', :js do let_it_be(:list) { create(:list, board: board, label: label, position: 0) } let_it_be(:user) { create(:user) } + let(:board_list_header) { first('[data-testid="board-list-header"]') } + let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') } + context 'authorized user' do before do project.add_maintainer(user) @@ -24,18 +27,18 @@ RSpec.describe 'Issue Boards new issue', :js do end it 'displays new issue button' do - expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + expect(first('.board')).to have_button('New issue', count: 1) end it 'does not display new issue button in closed list' do page.within('.board:nth-child(3)') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end it 'shows form when clicking button' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' expect(page).to have_selector('.board-new-issue-form') end @@ -43,7 +46,7 @@ RSpec.describe 'Issue Boards new issue', :js do it 'hides form when clicking cancel' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' expect(page).to have_selector('.board-new-issue-form') @@ -55,7 +58,7 @@ RSpec.describe 'Issue Boards new issue', :js do it 'creates new issue' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' end page.within(first('.board-new-issue-form')) do @@ -80,7 +83,7 @@ RSpec.describe 'Issue Boards new issue', :js do # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323446 xit 'shows sidebar when creating new issue' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' end page.within(first('.board-new-issue-form')) do @@ -95,7 +98,7 @@ RSpec.describe 'Issue Boards new issue', :js do it 'successfuly loads labels to be added to newly created issue' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' end page.within(first('.board-new-issue-form')) do @@ -109,12 +112,12 @@ RSpec.describe 'Issue Boards new issue', :js do find('.board-card').click end - page.within(first('[data-testid="issue-boards-sidebar"]')) do - find('.labels [data-testid="edit-button"]').click + page.within('[data-testid="sidebar-labels"]') do + click_button 'Edit' wait_for_requests - expect(page).to have_selector('.labels-select-contents-list .dropdown-content li a') + expect(page).to have_content 'Label 1' end end end @@ -126,70 +129,94 @@ RSpec.describe 'Issue Boards new issue', :js do end it 'displays new issue button in open list' do - expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + expect(first('.board')).to have_button('New issue', count: 1) end it 'does not display new issue button in label list' do page.within('.board:nth-child(2)') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end end context 'group boards' do let_it_be(:group) { create(:group, :public) } - let_it_be(:project) { create(:project, :public, namespace: group) } + let_it_be(:project) { create(:project, namespace: group, name: "root project") } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subproject1) { create(:project, group: subgroup, name: "sub project1") } + let_it_be(:subproject2) { create(:project, group: subgroup, name: "sub project2") } let_it_be(:group_board) { create(:board, group: group) } let_it_be(:project_label) { create(:label, project: project, name: 'label') } let_it_be(:list) { create(:list, board: group_board, label: project_label, position: 0) } context 'for unauthorized users' do - context 'when backlog does not exist' do - before do - sign_in(user) - visit group_board_path(group, group_board) - wait_for_requests - end + before do + visit group_board_path(group, group_board) + wait_for_requests + end + context 'when backlog does not exist' do it 'does not display new issue button in label list' do page.within('.board.is-draggable') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end end context 'when backlog list already exists' do - let!(:backlog_list) { create(:backlog_list, board: group_board) } - - before do - sign_in(user) - visit group_board_path(group, group_board) - wait_for_requests - end + let_it_be(:backlog_list) { create(:backlog_list, board: group_board) } it 'displays new issue button in open list' do - expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + expect(first('.board')).to have_button('New issue', count: 1) end it 'does not display new issue button in label list' do page.within('.board.is-draggable') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end end end context 'for authorized users' do - it 'display new issue button in label list' do - project = create(:project, namespace: group) + before do project.add_reporter(user) + subproject1.add_reporter(user) sign_in(user) visit group_board_path(group, group_board) wait_for_requests + end + + context 'when backlog does not exist' do + it 'display new issue button in label list' do + expect(board_list_header).to have_button('New issue') + end + end + + context 'project select dropdown' do + let_it_be(:backlog_list) { create(:backlog_list, board: group_board) } + + before do + page.within(board_list_header) do + click_button 'New issue' + end + + project_select_dropdown.click + + wait_for_requests + end + + it 'lists a project which is a direct descendant of the top-level group' do + expect(project_select_dropdown).to have_button("root project") + end + + it 'lists a project that belongs to a subgroup' do + expect(project_select_dropdown).to have_button("sub project1") + end - page.within('.board.is-draggable') do - expect(page).to have_selector('.issue-count-badge-add-button') + it "does not list projects to which user doesn't have access" do + expect(project_select_dropdown).not_to have_button("sub project2") end end end diff --git a/spec/finders/packages/group_or_project_package_finder_spec.rb b/spec/finders/packages/group_or_project_package_finder_spec.rb new file mode 100644 index 00000000000..aaeec8e70d2 --- /dev/null +++ b/spec/finders/packages/group_or_project_package_finder_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::GroupOrProjectPackageFinder do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:finder) { described_class.new(user, project) } + + describe 'execute' do + subject(:run_finder) { finder.execute } + + it { expect { run_finder }.to raise_error(NotImplementedError) } + end + + describe 'execute!' do + subject(:run_finder) { finder.execute! } + + it { expect { run_finder }.to raise_error(NotImplementedError) } + end +end diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb index d5f521ff895..13c603f1ec4 100644 --- a/spec/finders/packages/maven/package_finder_spec.rb +++ b/spec/finders/packages/maven/package_finder_spec.rb @@ -9,10 +9,9 @@ RSpec.describe ::Packages::Maven::PackageFinder do let_it_be_with_refind(:package) { create(:maven_package, project: project) } let(:param_path) { nil } - let(:param_project) { nil } - let(:param_group) { nil } + let(:project_or_group) { nil } let(:param_order_by_package_file) { false } - let(:finder) { described_class.new(param_path, user, project: param_project, group: param_group, order_by_package_file: param_order_by_package_file) } + let(:finder) { described_class.new(user, project_or_group, path: param_path, order_by_package_file: param_order_by_package_file) } before do group.add_developer(user) @@ -49,13 +48,13 @@ RSpec.describe ::Packages::Maven::PackageFinder do end context 'within the project' do - let(:param_project) { project } + let(:project_or_group) { project } it_behaves_like 'handling valid and invalid paths' end context 'within a group' do - let(:param_group) { group } + let(:project_or_group) { group } it_behaves_like 'handling valid and invalid paths' end @@ -77,7 +76,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) } let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) } - let(:param_group) { group } + let(:project_or_group) { group } let(:param_path) { package_name } before do @@ -116,7 +115,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do it_behaves_like 'Packages::Maven::PackageFinder examples' it 'uses CTE in the query' do - sql = described_class.new('some_path', user, group: group).send(:packages_with_path).to_sql + sql = described_class.new(user, group, path: package.maven_metadatum.path).send(:packages).to_sql expect(sql).to include('WITH "maven_metadata_by_path" AS') end diff --git a/spec/finders/packages/pypi/package_finder_spec.rb b/spec/finders/packages/pypi/package_finder_spec.rb new file mode 100644 index 00000000000..7d9eb8a5cd1 --- /dev/null +++ b/spec/finders/packages/pypi/package_finder_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Pypi::PackageFinder do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:project2) { create(:project, group: group) } + let_it_be(:package1) { create(:pypi_package, project: project) } + let_it_be(:package2) { create(:pypi_package, project: project) } + let_it_be(:package3) { create(:pypi_package, project: project2) } + + let(:package_file) { package2.package_files.first } + let(:params) do + { + filename: package_file.file_name, + sha256: package_file.file_sha256 + } + end + + describe 'execute' do + subject { described_class.new(user, scope, params).execute } + + context 'within a project' do + let(:scope) { project } + + it { is_expected.to eq(package2) } + end + + context 'within a group' do + let(:scope) { group } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + + context 'user with access' do + before do + project.add_developer(user) + end + + it { is_expected.to eq(package2) } + end + end + end +end diff --git a/spec/finders/packages/pypi/packages_finder_spec.rb b/spec/finders/packages/pypi/packages_finder_spec.rb new file mode 100644 index 00000000000..a69c2317261 --- /dev/null +++ b/spec/finders/packages/pypi/packages_finder_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Pypi::PackagesFinder do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:project2) { create(:project, group: group) } + let_it_be(:package1) { create(:pypi_package, project: project) } + let_it_be(:package2) { create(:pypi_package, project: project) } + let_it_be(:package3) { create(:pypi_package, name: package2.name, project: project) } + let_it_be(:package4) { create(:pypi_package, name: package2.name, project: project2) } + + let(:package_name) { package2.name } + + describe 'execute!' do + subject { described_class.new(user, scope, package_name: package_name).execute! } + + shared_examples 'when no package is found' do + context 'non-existing package' do + let(:package_name) { 'none' } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end + + shared_examples 'when package_name param is a non-normalized name' do + context 'non-existing package' do + let(:package_name) { package2.name.upcase.tr('-', '.') } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end + + context 'within a project' do + let(:scope) { project } + + it { is_expected.to contain_exactly(package2, package3) } + + it_behaves_like 'when no package is found' + it_behaves_like 'when package_name param is a non-normalized name' + end + + context 'within a group' do + let(:scope) { group } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + + context 'user with access to only one project' do + before do + project2.add_developer(user) + end + + it { is_expected.to contain_exactly(package4) } + + it_behaves_like 'when no package is found' + it_behaves_like 'when package_name param is a non-normalized name' + + context ' user with access to multiple projects' do + before do + project.add_developer(user) + end + + it { is_expected.to contain_exactly(package2, package3, package4) } + end + end + end + end +end diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 7851f633629..57f0b852ff8 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -920,6 +920,7 @@ export default { cancel_path: '/root/ci-mock/-/jobs/4757/cancel', new_issue_path: '/root/ci-mock/issues/new', playable: false, + complete: true, created_at: threeWeeksAgo.toISOString(), updated_at: threeWeeksAgo.toISOString(), finished_at: threeWeeksAgo.toISOString(), diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index 472f2a8b211..28fe3b67e7b 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -8,6 +8,7 @@ export const mockPipelineResponse = { __typename: 'Pipeline', id: 163, iid: '22', + complete: true, usesNeeds: true, downstream: null, upstream: null, @@ -570,6 +571,7 @@ export const wrappedPipelineReturn = { __typename: 'Pipeline', id: 'gid://gitlab/Ci::Pipeline/175', iid: '38', + complete: true, usesNeeds: true, downstream: { __typename: 'PipelineConnection', diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap new file mode 100644 index 00000000000..cef1dff3335 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReadyToMerge with a mismatched SHA warns the user to refresh to review 1`] = `"<gl-sprintf-stub message=\\"New changes were added. %{linkStart}Reload the page to review them%{linkEnd}\\"></gl-sprintf-stub>"`; diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 7202f327683..85a42946325 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,13 +1,13 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import simplePoll from '~/lib/utils/simple_poll'; import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; -import { MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; +import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; import eventHub from '~/vue_merge_request_widget/event_hub'; jest.mock('~/lib/utils/simple_poll', () => @@ -58,11 +58,9 @@ const createTestService = () => ({ poll: jest.fn().mockResolvedValue(), }); +let wrapper; const createComponent = (customConfig = {}) => { - const Component = Vue.extend(ReadyToMerge); - - return new Component({ - el: document.createElement('div'), + wrapper = shallowMount(ReadyToMerge, { propsData: { mr: createTestMr(customConfig), service: createTestService(), @@ -71,277 +69,207 @@ const createComponent = (customConfig = {}) => { }; describe('ReadyToMerge', () => { - let vm; - - beforeEach(() => { - vm = createComponent(); - }); - afterEach(() => { - vm.$destroy(); - }); - - describe('props', () => { - it('should have props', () => { - const { mr, service } = ReadyToMerge.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); - }); - }); - - describe('data', () => { - it('should have default data', () => { - expect(vm.mergeWhenBuildSucceeds).toBeFalsy(); - expect(vm.useCommitMessageWithDescription).toBeFalsy(); - expect(vm.showCommitMessageEditor).toBeFalsy(); - expect(vm.isMakingRequest).toBeFalsy(); - expect(vm.isMergingImmediately).toBeFalsy(); - expect(vm.commitMessage).toBe(vm.mr.commitMessage); - }); + wrapper.destroy(); }); describe('computed', () => { describe('isAutoMergeAvailable', () => { it('should return true when at least one merge strategy is available', () => { - vm.mr.availableAutoMergeStrategies = [MWPS_MERGE_STRATEGY]; + createComponent(); - expect(vm.isAutoMergeAvailable).toBe(true); + expect(wrapper.vm.isAutoMergeAvailable).toBe(true); }); it('should return false when no merge strategies are available', () => { - vm.mr.availableAutoMergeStrategies = []; + createComponent({ mr: { availableAutoMergeStrategies: [] } }); - expect(vm.isAutoMergeAvailable).toBe(false); + expect(wrapper.vm.isAutoMergeAvailable).toBe(false); }); }); describe('status', () => { it('defaults to success', () => { - Vue.set(vm.mr, 'pipeline', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ mr: { pipeline: true, availableAutoMergeStrategies: [] } }); - expect(vm.status).toEqual('success'); + expect(wrapper.vm.status).toEqual('success'); }); it('returns failed when MR has CI but also has an unknown status', () => { - Vue.set(vm.mr, 'hasCI', true); + createComponent({ mr: { hasCI: true } }); - expect(vm.status).toEqual('failed'); + expect(wrapper.vm.status).toEqual('failed'); }); it('returns default when MR has no pipeline', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ mr: { availableAutoMergeStrategies: [] } }); - expect(vm.status).toEqual('success'); + expect(wrapper.vm.status).toEqual('success'); }); it('returns pending when pipeline is active', () => { - Vue.set(vm.mr, 'pipeline', {}); - Vue.set(vm.mr, 'isPipelineActive', true); + createComponent({ mr: { pipeline: {}, isPipelineActive: true } }); - expect(vm.status).toEqual('pending'); + expect(wrapper.vm.status).toEqual('pending'); }); it('returns failed when pipeline is failed', () => { - Vue.set(vm.mr, 'pipeline', {}); - Vue.set(vm.mr, 'isPipelineFailed', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ + mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] }, + }); - expect(vm.status).toEqual('failed'); + expect(wrapper.vm.status).toEqual('failed'); }); }); describe('mergeButtonVariant', () => { it('defaults to success class', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ + mr: { availableAutoMergeStrategies: [] }, + }); - expect(vm.mergeButtonVariant).toEqual('success'); + expect(wrapper.vm.mergeButtonVariant).toEqual('success'); }); it('returns success class for success status', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - Vue.set(vm.mr, 'pipeline', true); + createComponent({ + mr: { availableAutoMergeStrategies: [], pipeline: true }, + }); - expect(vm.mergeButtonVariant).toEqual('success'); + expect(wrapper.vm.mergeButtonVariant).toEqual('success'); }); it('returns info class for pending status', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [MTWPS_MERGE_STRATEGY]); + createComponent(); - expect(vm.mergeButtonVariant).toEqual('info'); + expect(wrapper.vm.mergeButtonVariant).toEqual('info'); }); it('returns danger class for failed status', () => { - vm.mr.hasCI = true; + createComponent({ mr: { hasCI: true } }); - expect(vm.mergeButtonVariant).toEqual('danger'); + expect(wrapper.vm.mergeButtonVariant).toEqual('danger'); }); }); describe('status icon', () => { it('defaults to tick icon', () => { - expect(vm.iconClass).toEqual('success'); + createComponent(); + + expect(wrapper.vm.iconClass).toEqual('success'); }); it('shows tick for success status', () => { - vm.mr.pipeline = true; + createComponent({ mr: { pipeline: true } }); - expect(vm.iconClass).toEqual('success'); + expect(wrapper.vm.iconClass).toEqual('success'); }); it('shows tick for pending status', () => { - vm.mr.pipeline = {}; - vm.mr.isPipelineActive = true; + createComponent({ mr: { pipeline: {}, isPipelineActive: true } }); - expect(vm.iconClass).toEqual('success'); - }); - - it('shows warning icon for failed status', () => { - vm.mr.hasCI = true; - - expect(vm.iconClass).toEqual('warning'); - }); - - it('shows warning icon for merge not allowed', () => { - vm.mr.hasCI = true; - - expect(vm.iconClass).toEqual('warning'); + expect(wrapper.vm.iconClass).toEqual('success'); }); }); describe('mergeButtonText', () => { it('should return "Merge" when no auto merge strategies are available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ mr: { availableAutoMergeStrategies: [] } }); - expect(vm.mergeButtonText).toEqual('Merge'); + expect(wrapper.vm.mergeButtonText).toEqual('Merge'); }); - it('should return "Merge in progress"', () => { - Vue.set(vm, 'isMergingImmediately', true); + it('should return "Merge in progress"', async () => { + createComponent(); + + wrapper.setData({ isMergingImmediately: true }); + + await Vue.nextTick(); - expect(vm.mergeButtonText).toEqual('Merge in progress'); + expect(wrapper.vm.mergeButtonText).toEqual('Merge in progress'); }); it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => { - Vue.set(vm, 'isMergingImmediately', false); - Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY); + createComponent({ + mr: { isMergingImmediately: false, preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY }, + }); - expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds'); + expect(wrapper.vm.mergeButtonText).toEqual('Merge when pipeline succeeds'); }); }); describe('autoMergeText', () => { it('should return Merge when pipeline succeeds', () => { - Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY); + createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } }); - expect(vm.autoMergeText).toEqual('Merge when pipeline succeeds'); + expect(wrapper.vm.autoMergeText).toEqual('Merge when pipeline succeeds'); }); }); describe('shouldShowMergeImmediatelyDropdown', () => { it('should return false if no pipeline is active', () => { - Vue.set(vm.mr, 'isPipelineActive', false); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); + createComponent({ + mr: { isPipelineActive: false, onlyAllowMergeIfPipelineSucceeds: false }, + }); - expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); + expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); it('should return false if "Pipelines must succeed" is enabled for the current project', () => { - Vue.set(vm.mr, 'isPipelineActive', true); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + createComponent({ mr: { isPipelineActive: true, onlyAllowMergeIfPipelineSucceeds: true } }); - expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); - }); - - it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => { - Vue.set(vm.mr, 'isPipelineActive', true); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - - expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true); + expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); }); describe('isMergeButtonDisabled', () => { it('should return false with initial data', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); + createComponent({ mr: { isMergeAllowed: true } }); - expect(vm.isMergeButtonDisabled).toBe(false); + expect(wrapper.vm.isMergeButtonDisabled).toBe(false); }); it('should return true when there is no commit message', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm, 'commitMessage', ''); + createComponent({ mr: { isMergeAllowed: true, commitMessage: '' } }); - expect(vm.isMergeButtonDisabled).toBe(true); + expect(wrapper.vm.isMergeButtonDisabled).toBe(true); }); it('should return true if merge is not allowed', () => { - Vue.set(vm.mr, 'isMergeAllowed', false); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + createComponent({ + mr: { + isMergeAllowed: false, + availableAutoMergeStrategies: [], + onlyAllowMergeIfPipelineSucceeds: true, + }, + }); - expect(vm.isMergeButtonDisabled).toBe(true); + expect(wrapper.vm.isMergeButtonDisabled).toBe(true); }); - it('should return true when the vm instance is making request', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm, 'isMakingRequest', true); + it('should return true when the vm instance is making request', async () => { + createComponent({ mr: { isMergeAllowed: true } }); - expect(vm.isMergeButtonDisabled).toBe(true); - }); - }); + wrapper.setData({ isMakingRequest: true }); - describe('isMergeImmediatelyDangerous', () => { - it('should always return false in CE', () => { - expect(vm.isMergeImmediatelyDangerous).toBe(false); + await Vue.nextTick(); + + expect(wrapper.vm.isMergeButtonDisabled).toBe(true); }); }); }); describe('methods', () => { - describe('shouldShowMergeControls', () => { - it('should return false when an external pipeline is running and required to succeed', () => { - Vue.set(vm.mr, 'isMergeAllowed', false); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - - expect(vm.shouldShowMergeControls).toBe(false); - }); - - it('should return true when the build succeeded or build not required to succeed', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - - expect(vm.shouldShowMergeControls).toBe(true); - }); - - it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => { - Vue.set(vm.mr, 'isMergeAllowed', false); - Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]); - - expect(vm.shouldShowMergeControls).toBe(true); - }); - - it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]); - - expect(vm.shouldShowMergeControls).toBe(true); - }); - }); - describe('updateMergeCommitMessage', () => { it('should revert flag and change commitMessage', () => { - expect(vm.commitMessage).toEqual(commitMessage); - vm.updateMergeCommitMessage(true); + createComponent(); + + wrapper.vm.updateMergeCommitMessage(true); - expect(vm.commitMessage).toEqual(commitMessageWithDescription); - vm.updateMergeCommitMessage(false); + expect(wrapper.vm.commitMessage).toEqual(commitMessageWithDescription); + wrapper.vm.updateMergeCommitMessage(false); - expect(vm.commitMessage).toEqual(commitMessage); + expect(wrapper.vm.commitMessage).toEqual(commitMessage); }); }); @@ -356,23 +284,26 @@ describe('ReadyToMerge', () => { }); it('should handle merge when pipeline succeeds', (done) => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest - .spyOn(vm.service, 'merge') + .spyOn(wrapper.vm.service, 'merge') .mockReturnValue(returnPromise('merge_when_pipeline_succeeds')); - vm.removeSourceBranch = false; - vm.handleMergeButtonClick(true); + wrapper.setData({ removeSourceBranch: false }); + + wrapper.vm.handleMergeButtonClick(true); setImmediate(() => { - expect(vm.isMakingRequest).toBeTruthy(); + expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); - const params = vm.service.merge.mock.calls[0][0]; + const params = wrapper.vm.service.merge.mock.calls[0][0]; expect(params).toEqual( expect.objectContaining({ - sha: vm.mr.sha, - commit_message: vm.mr.commitMessage, + sha: wrapper.vm.mr.sha, + commit_message: wrapper.vm.mr.commitMessage, should_remove_source_branch: false, auto_merge_strategy: 'merge_when_pipeline_succeeds', }), @@ -382,15 +313,17 @@ describe('ReadyToMerge', () => { }); it('should handle merge failed', (done) => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('failed')); - vm.handleMergeButtonClick(false, true); + jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('failed')); + wrapper.vm.handleMergeButtonClick(false, true); setImmediate(() => { - expect(vm.isMakingRequest).toBeTruthy(); + expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined); - const params = vm.service.merge.mock.calls[0][0]; + const params = wrapper.vm.service.merge.mock.calls[0][0]; expect(params.should_remove_source_branch).toBeTruthy(); expect(params.auto_merge_strategy).toBeUndefined(); @@ -399,15 +332,17 @@ describe('ReadyToMerge', () => { }); it('should handle merge action accepted case', (done) => { - jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('success')); - jest.spyOn(vm, 'initiateMergePolling').mockImplementation(() => {}); - vm.handleMergeButtonClick(); + createComponent(); + + jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('success')); + jest.spyOn(wrapper.vm, 'initiateMergePolling').mockImplementation(() => {}); + wrapper.vm.handleMergeButtonClick(); setImmediate(() => { - expect(vm.isMakingRequest).toBeTruthy(); - expect(vm.initiateMergePolling).toHaveBeenCalled(); + expect(wrapper.vm.isMakingRequest).toBeTruthy(); + expect(wrapper.vm.initiateMergePolling).toHaveBeenCalled(); - const params = vm.service.merge.mock.calls[0][0]; + const params = wrapper.vm.service.merge.mock.calls[0][0]; expect(params.should_remove_source_branch).toBeTruthy(); expect(params.auto_merge_strategy).toBeUndefined(); @@ -418,128 +353,31 @@ describe('ReadyToMerge', () => { describe('initiateMergePolling', () => { it('should call simplePoll', () => { - vm.initiateMergePolling(); + createComponent(); + + wrapper.vm.initiateMergePolling(); expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 }); }); it('should call handleMergePolling', () => { - jest.spyOn(vm, 'handleMergePolling').mockImplementation(() => {}); - - vm.initiateMergePolling(); - - expect(vm.handleMergePolling).toHaveBeenCalled(); - }); - }); - - describe('handleMergePolling', () => { - const returnPromise = (state) => - new Promise((resolve) => { - resolve({ - data: { - state, - source_branch_exists: true, - }, - }); - }); - - beforeEach(() => { - loadFixtures('merge_requests/merge_request_of_current_user.html'); - }); - - it('should call start and stop polling when MR merged', (done) => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - let cpc = false; // continuePollingCalled - let spc = false; // stopPollingCalled - - vm.handleMergePolling( - () => { - cpc = true; - }, - () => { - spc = true; - }, - ); - setImmediate(() => { - expect(vm.service.poll).toHaveBeenCalled(); - expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); - expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent'); - expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled(); - expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); - expect(cpc).toBeFalsy(); - expect(spc).toBeTruthy(); + createComponent(); - done(); - }); - }); - - it('updates status box', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - vm.handleMergePolling( - () => {}, - () => {}, - ); - - setImmediate(() => { - const statusBox = document.querySelector('.status-box'); - - expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy(); - expect(statusBox.textContent).toContain('Merged'); - - done(); - }); - }); - - it('updates merge request count badge', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - vm.handleMergePolling( - () => {}, - () => {}, - ); + jest.spyOn(wrapper.vm, 'handleMergePolling').mockImplementation(() => {}); - setImmediate(() => { - expect(document.querySelector('.js-merge-counter').textContent).toBe('0'); - - done(); - }); - }); - - it('should continue polling until MR is merged', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('some_other_state')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - let cpc = false; // continuePollingCalled - let spc = false; // stopPollingCalled - - vm.handleMergePolling( - () => { - cpc = true; - }, - () => { - spc = true; - }, - ); - setImmediate(() => { - expect(cpc).toBeTruthy(); - expect(spc).toBeFalsy(); + wrapper.vm.initiateMergePolling(); - done(); - }); + expect(wrapper.vm.handleMergePolling).toHaveBeenCalled(); }); }); describe('initiateRemoveSourceBranchPolling', () => { it('should emit event and call simplePoll', () => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - vm.initiateRemoveSourceBranchPolling(); + wrapper.vm.initiateRemoveSourceBranchPolling(); expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]); expect(simplePoll).toHaveBeenCalled(); @@ -557,13 +395,15 @@ describe('ReadyToMerge', () => { }); it('should call start and stop polling when MR merged', (done) => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(false)); + jest.spyOn(wrapper.vm.service, 'poll').mockReturnValue(returnPromise(false)); let cpc = false; // continuePollingCalled let spc = false; // stopPollingCalled - vm.handleRemoveBranchPolling( + wrapper.vm.handleRemoveBranchPolling( () => { cpc = true; }, @@ -572,7 +412,7 @@ describe('ReadyToMerge', () => { }, ); setImmediate(() => { - expect(vm.service.poll).toHaveBeenCalled(); + expect(wrapper.vm.service.poll).toHaveBeenCalled(); const args = eventHub.$emit.mock.calls[0]; @@ -590,12 +430,14 @@ describe('ReadyToMerge', () => { }); it('should continue polling until MR is merged', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(true)); + createComponent(); + + jest.spyOn(wrapper.vm.service, 'poll').mockReturnValue(returnPromise(true)); let cpc = false; // continuePollingCalled let spc = false; // stopPollingCalled - vm.handleRemoveBranchPolling( + wrapper.vm.handleRemoveBranchPolling( () => { cpc = true; }, @@ -616,49 +458,26 @@ describe('ReadyToMerge', () => { describe('Remove source branch checkbox', () => { describe('when user can merge but cannot delete branch', () => { it('should be disabled in the rendered output', () => { - const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + createComponent(); - expect(checkboxElement).toBeNull(); + expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false); }); }); describe('when user can merge and can delete branch', () => { beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { canRemoveSourceBranch: true }, }); }); it('isRemoveSourceBranchButtonDisabled should be false', () => { - expect(vm.isRemoveSourceBranchButtonDisabled).toBe(false); - }); - - it('removed source branch should be enabled in rendered output', () => { - const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); - - expect(checkboxElement).not.toBeNull(); + expect(wrapper.find('#remove-source-branch-input').props('disabled')).toBe(undefined); }); }); }); describe('render children components', () => { - let wrapper; - const localVue = createLocalVue(); - - const createLocalComponent = (customConfig = {}) => { - wrapper = shallowMount(localVue.extend(ReadyToMerge), { - localVue, - propsData: { - mr: createTestMr(customConfig), - service: createTestService(), - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); const findCommitsHeaderElement = () => wrapper.find(CommitsHeader); const findCommitEditElements = () => wrapper.findAll(CommitEdit); @@ -667,7 +486,7 @@ describe('ReadyToMerge', () => { describe('squash checkbox', () => { it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true }, }); @@ -675,13 +494,13 @@ describe('ReadyToMerge', () => { }); it('should not be rendered when squash before merge is disabled', () => { - createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); expect(findCheckboxElement().exists()).toBeFalsy(); }); it('should not be rendered when there is only 1 commit', () => { - createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + createComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); expect(findCheckboxElement().exists()).toBeFalsy(); }); @@ -695,7 +514,7 @@ describe('ReadyToMerge', () => { `( 'is $state when squashIsReadonly returns $expectation ', ({ squashState, prop, expectation }) => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true, [squashState]: expectation }, }); @@ -704,7 +523,7 @@ describe('ReadyToMerge', () => { ); it('is not rendered for "Do not allow" option', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true, @@ -720,14 +539,14 @@ describe('ReadyToMerge', () => { describe('commits count collapsible header', () => { it('should be rendered when fast-forward is disabled', () => { - createLocalComponent(); + createComponent(); expect(findCommitsHeaderElement().exists()).toBeTruthy(); }); describe('when fast-forward is enabled', () => { it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, enableSquashBeforeMerge: true, @@ -740,7 +559,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if squash before merge is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, enableSquashBeforeMerge: false, @@ -753,7 +572,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if squash is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: false, @@ -766,7 +585,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if commits count is 1', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: true, @@ -783,7 +602,7 @@ describe('ReadyToMerge', () => { describe('commits edit components', () => { describe('when fast-forward merge is enabled', () => { it('should not be rendered if squash is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: false, @@ -796,7 +615,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if squash before merge is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: true, @@ -809,7 +628,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if there is only one commit', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: true, @@ -822,7 +641,7 @@ describe('ReadyToMerge', () => { }); it('should have one edit component if squash is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squashIsSelected: true, @@ -837,13 +656,13 @@ describe('ReadyToMerge', () => { }); it('should have one edit component when squash is disabled', () => { - createLocalComponent(); + createComponent(); expect(findCommitEditElements().length).toBe(1); }); it('should have two edit components when squash is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, squashIsSelected: true, @@ -855,7 +674,7 @@ describe('ReadyToMerge', () => { }); it('should have one edit components when squash is enabled and there is 1 commit only', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 1, squash: true, @@ -867,13 +686,13 @@ describe('ReadyToMerge', () => { }); it('should have correct edit merge commit label', () => { - createLocalComponent(); + createComponent(); expect(findFirstCommitEditLabel()).toBe('Merge commit message'); }); it('should have correct edit squash commit label', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, squashIsSelected: true, @@ -887,13 +706,13 @@ describe('ReadyToMerge', () => { describe('commits dropdown', () => { it('should not be rendered if squash is disabled', () => { - createLocalComponent(); + createComponent(); expect(findCommitDropdownElement().exists()).toBeFalsy(); }); it('should be rendered if squash is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { enableSquashBeforeMerge: true, squashIsSelected: true, commitsCount: 2 }, }); @@ -902,83 +721,38 @@ describe('ReadyToMerge', () => { }); }); - describe('Merge controls', () => { - describe('when allowed to merge', () => { - beforeEach(() => { - vm = createComponent({ - mr: { isMergeAllowed: true, canRemoveSourceBranch: true }, - }); - }); - - it('shows remove source branch checkbox', () => { - expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).not.toBeNull(); - }); - - it('shows modify commit message button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); - }); - - it('does not show message about needing to resolve items', () => { - expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull(); - }); - }); - - describe('when not allowed to merge', () => { - beforeEach(() => { - vm = createComponent({ - mr: { isMergeAllowed: false }, - }); - }); - - it('does not show remove source branch checkbox', () => { - expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull(); - }); - - it('shows message to resolve all items before being allowed to merge', () => { - expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined(); - }); - }); - }); - describe('Merge request project settings', () => { describe('when the merge commit merge method is enabled', () => { beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { ffOnlyEnabled: false }, }); }); it('should not show fast forward message', () => { - expect(vm.$el.querySelector('.mr-fast-forward-message')).toBeNull(); - }); - - it('should show "Modify commit message" button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); + expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(false); }); }); describe('when the fast-forward merge method is enabled', () => { beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { ffOnlyEnabled: true }, }); }); it('should show fast forward message', () => { - expect(vm.$el.querySelector('.mr-fast-forward-message')).toBeDefined(); - }); - - it('should not show "Modify commit message" button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); + expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(true); }); }); }); describe('with a mismatched SHA', () => { - const findMismatchShaBlock = () => vm.$el.querySelector('.js-sha-mismatch'); + const findMismatchShaBlock = () => wrapper.find('.js-sha-mismatch'); + const findMismatchShaTextBlock = () => findMismatchShaBlock().find(GlSprintf); beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { isSHAMismatch: true, mergeRequestDiffsPath: '/merge_requests/1/diffs', @@ -987,17 +761,11 @@ describe('ReadyToMerge', () => { }); it('displays a warning message', () => { - expect(findMismatchShaBlock()).toExist(); + expect(findMismatchShaBlock().exists()).toBe(true); }); it('warns the user to refresh to review', () => { - expect(findMismatchShaBlock().textContent.trim()).toBe( - 'New changes were added. Reload the page to review them', - ); - }); - - it('displays link to the diffs tab', () => { - expect(findMismatchShaBlock().querySelector('a').href).toContain(vm.mr.mergeRequestDiffsPath); + expect(findMismatchShaTextBlock().element.outerHTML).toMatchSnapshot(); }); }); }); diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb index 23d4d86c79a..2c9c3a47650 100644 --- a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb @@ -41,6 +41,20 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do it 'returns the ordered versions' do expect(result.to_a).to eq(all_versions) end + + context 'loading associations' do + it 'prevents N+1 queries when loading author' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + resolve_versions(object).items.map(&:author) + end.count + + create_list(:design_version, 3, issue: issue) + + expect do + resolve_versions(object).items.map(&:author) + end.not_to exceed_all_query_limit(control_count) + end + end end context 'when constrained' do diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index c67e86a7ee1..35d48229fa4 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Types::Ci::PipelineType do it 'contains attributes related to a pipeline' do expected_fields = %w[ - id iid sha before_sha status detailed_status config_source + id iid sha before_sha complete status detailed_status config_source duration queued_duration coverage created_at updated_at started_at finished_at committed_at stages user retryable cancelable jobs source_job job downstream diff --git a/spec/graphql/types/design_management/version_type_spec.rb b/spec/graphql/types/design_management/version_type_spec.rb index 017cc1775a1..62335a65fdf 100644 --- a/spec/graphql/types/design_management/version_type_spec.rb +++ b/spec/graphql/types/design_management/version_type_spec.rb @@ -6,7 +6,7 @@ RSpec.describe GitlabSchema.types['DesignVersion'] do it { expect(described_class).to require_graphql_authorizations(:read_design) } it 'has the expected fields' do - expected_fields = %i[id sha designs design_at_version designs_at_version] + expected_fields = %i[id sha designs design_at_version designs_at_version author created_at] expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/helpers/webpack_helper_spec.rb b/spec/helpers/webpack_helper_spec.rb new file mode 100644 index 00000000000..f9386c99dc3 --- /dev/null +++ b/spec/helpers/webpack_helper_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebpackHelper do + let(:source) { 'foo.js' } + let(:asset_path) { "/assets/webpack/#{source}" } + + describe '#prefetch_link_tag' do + it 'returns prefetch link tag' do + expect(helper.prefetch_link_tag(source)).to eq("<link rel=\"prefetch\" href=\"/#{source}\">") + end + end + + describe '#webpack_preload_asset_tag' do + before do + allow(Gitlab::Webpack::Manifest).to receive(:asset_paths).and_return([asset_path]) + end + + it 'preloads the resource by default' do + expect(helper).to receive(:preload_link_tag).with(asset_path, {}).and_call_original + + output = helper.webpack_preload_asset_tag(source) + + expect(output).to eq("<link rel=\"preload\" href=\"#{asset_path}\" as=\"script\" type=\"text/javascript\">") + end + + it 'prefetches the resource if explicitly asked' do + expect(helper).to receive(:prefetch_link_tag).with(asset_path).and_call_original + + output = helper.webpack_preload_asset_tag(source, prefetch: true) + + expect(output).to eq("<link rel=\"prefetch\" href=\"#{asset_path}\">") + end + end +end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index cdb123573f1..3c4769764d5 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -602,6 +602,34 @@ RSpec.describe Ci::JobArtifact do end end + context 'FastDestroyAll' do + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + let!(:job_artifact) { create(:ci_job_artifact, :archive, job: job) } + let(:subjects) { pipeline.job_artifacts } + + describe '.use_fast_destroy' do + it 'performs cascading delete with fast_destroy_all' do + expect(Ci::DeletedObject.count).to eq(0) + expect(subjects.count).to be > 0 + + expect { pipeline.destroy! }.not_to raise_error + + expect(subjects.count).to eq(0) + expect(Ci::DeletedObject.count).to be > 0 + end + + it 'updates project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(project, :build_artifacts_size, -job_artifact.file.size) + + pipeline.destroy! + end + end + end + def file_type_limit_failure_message(type, limit_name) <<~MSG The artifact type `#{type}` is missing its counterpart plan limit which is expected to be named `#{limit_name}`. diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7bb344a4ba3..e5ed8d89145 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6936,6 +6936,32 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#increment_statistic_value' do + let(:project) { build_stubbed(:project) } + + subject(:increment) do + project.increment_statistic_value(:build_artifacts_size, -10) + end + + it 'increments the value' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .with(project, :build_artifacts_size, -10) + + increment + end + + context 'when the project is scheduled for removal' do + let(:project) { build_stubbed(:project, pending_delete: true) } + + it 'does not increment the value' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + increment + end + end + end + def finish_job(export_job) export_job.start export_job.finish diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 44014f93444..579a9e664cf 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -620,16 +620,12 @@ RSpec.describe WikiPage do end describe "#versions" do - include_context 'subject is persisted page' + let(:subject) { create_wiki_page } it "returns an array of all commits for the page" do - 3.times { |i| subject.update(content: "content #{i}") } - - expect(subject.versions.count).to eq(4) - end - - it 'returns instances of WikiPageVersion' do - expect(subject.versions).to all( be_a(Gitlab::Git::WikiPageVersion) ) + expect do + 3.times { |i| subject.update(content: "content #{i}") } + end.to change { subject.versions.count }.by(3) end end @@ -777,8 +773,11 @@ RSpec.describe WikiPage do end describe '#historical?' do - include_context 'subject is persisted page' + let!(:container) { create(:project) } + + subject { create_wiki_page } + let(:wiki) { subject.wiki } let(:old_version) { subject.versions.last.id } let(:old_page) { wiki.find_page(subject.title, old_version) } let(:latest_version) { subject.versions.first.id } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 540647fb699..cc837e7544c 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -60,7 +60,7 @@ RSpec.describe ProjectPolicy do end it 'does not include the issues permissions' do - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident end it 'disables boards and lists permissions' do @@ -72,7 +72,7 @@ RSpec.describe ProjectPolicy do it 'does not include the issues permissions' do create(:jira_service, project: project) - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident end end end diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb index ee0085718b3..9d98498ca8a 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -33,6 +33,7 @@ RSpec.describe 'Getting versions related to an issue' do let(:version_params) { nil } let(:version_query_fields) { ['edges { node { sha } }'] } + let(:edges_path) { %w[project issue designCollection versions edges] } let(:project) { issue.project } let(:current_user) { owner } @@ -50,8 +51,7 @@ RSpec.describe 'Getting versions related to an issue' do end def response_values(data = graphql_data, key = 'sha') - path = %w[project issue designCollection versions edges] - data.dig(*path).map { |e| e.dig('node', key) } + data.dig(*edges_path).map { |e| e.dig('node', key) } end before do @@ -64,6 +64,19 @@ RSpec.describe 'Getting versions related to an issue' do expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha)) end + context 'with all fields requested' do + let(:version_query_fields) do + ['edges { node { id sha createdAt author { id } } }'] + end + + it 'returns correct data' do + post_graphql(query, current_user: current_user) + + keys = graphql_data.dig(*edges_path).first['node'].keys + expect(keys).to match_array(%w(id sha createdAt author)) + end + end + describe 'filter by sha' do let(:sha) { version_b.sha } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e4eac9ee174..a13db1bb414 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2154,7 +2154,7 @@ RSpec.describe API::MergeRequests do end end - describe 'PUT /projects/:id/merge_reuests/:merge_request_iid' do + describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do it_behaves_like 'issuable update endpoint' do let(:entity) { merge_request } end @@ -2176,6 +2176,68 @@ RSpec.describe API::MergeRequests do end end + context 'when assignee_id=user2.id' do + let(:params) do + { + assignee_id: user2.id + } + end + + it 'sets the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to contain_exactly( + a_hash_including('name' => user2.name) + ) + end + end + + context 'when only assignee_ids are provided, and the list is empty' do + let(:params) do + { + assignee_ids: [] + } + end + + it 'clears the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to be_empty + end + end + + context 'when only assignee_ids are provided, and the list contains the sentinel value' do + let(:params) do + { + assignee_ids: [0] + } + end + + it 'clears the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to be_empty + end + end + + context 'when only assignee_id=0' do + let(:params) do + { + assignee_id: 0 + } + end + + it 'clears the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to be_empty + end + end + context 'accepts reviewer_ids' do let(:params) do { diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 1cbf1914c0c..f31cfcb8499 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -21,6 +21,10 @@ RSpec.describe JobEntity do subject { entity.as_json } + it 'contains complete to indicate if a pipeline is completed' do + expect(subject).to include(:complete) + end + it 'contains paths to job page action' do expect(subject).to include(:build_path) end diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index f226a129fac..302233cea5a 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe ::Ci::DestroyPipelineService do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.id) } subject { described_class.new(project, user).execute(pipeline) } @@ -60,6 +61,10 @@ RSpec.describe ::Ci::DestroyPipelineService do expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) end + + it 'inserts deleted objects for object storage files' do + expect { subject }.to change { Ci::DeletedObject.count } + end end end end diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb new file mode 100644 index 00000000000..b1a4741851b --- /dev/null +++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do + let(:artifacts) { Ci::JobArtifact.all } + let(:service) { described_class.new(artifacts) } + + let_it_be(:artifact, refind: true) do + create(:ci_job_artifact) + end + + before do + artifact.file = fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') + artifact.save! + end + + describe '#destroy_records' do + it 'removes artifacts without updating statistics' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + expect { service.destroy_records }.to change { Ci::JobArtifact.count } + end + + context 'when there are no artifacts' do + let(:artifacts) { Ci::JobArtifact.none } + + it 'does not raise error' do + expect { service.destroy_records }.not_to raise_error + end + end + end + + describe '#update_statistics' do + before do + service.destroy_records + end + + it 'updates project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(artifact.project, :build_artifacts_size, -artifact.file.size) + + service.update_statistics + end + + context 'when there are no artifacts' do + let(:artifacts) { Ci::JobArtifact.none } + + it 'does not raise error' do + expect { service.update_statistics }.not_to raise_error + end + end + end +end diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 52aaf73d67e..2cedbf93d74 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::DestroyBatchService do - include ExclusiveLeaseHelpers - let(:artifacts) { Ci::JobArtifact.all } let(:service) { described_class.new(artifacts, pick_up_at: Time.current) } @@ -25,14 +23,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do expect { subject }.to change { Ci::DeletedObject.count }.by(1) end - it 'resets project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact.project, :build_artifacts_size, -artifact.file.size) - .and_call_original - - execute - end - it 'does not remove the files' do expect { execute }.not_to change { artifact.file.exists? } end @@ -44,6 +34,29 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do execute end + + context 'ProjectStatistics' do + it 'resets project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(artifact.project, :build_artifacts_size, -artifact.file.size) + .and_call_original + + execute + end + + context 'with update_stats: false' do + it 'does not update project statistics' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + service.execute(update_stats: false) + end + + it 'returns size statistics' do + expect(service.execute(update_stats: false)).to match( + a_hash_including(statistics_updates: { artifact.project => -artifact.file.size })) + end + end + end end context 'when failed to destroy artifact' do @@ -65,16 +78,12 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do context 'when there are no artifacts' do let(:artifacts) { Ci::JobArtifact.none } - before do - artifact.destroy! - end - it 'does not raise error' do expect { execute }.not_to raise_error end it 'reports the number of destroyed artifacts' do - is_expected.to eq(destroyed_artifacts_count: 0, status: :success) + is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success) end end end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index d0f228fb3d9..3f506ec58b0 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -184,9 +184,9 @@ RSpec.describe Issues::BuildService do end it 'cannot set invalid type' do - expect do - build_issue(issue_type: 'invalid type') - end.to raise_error(ArgumentError, "'invalid type' is not a valid issue_type") + issue = build_issue(issue_type: 'invalid type') + + expect(issue).to be_issue end end end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index aa9eb0e6a0d..3ea2727dc60 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -25,6 +25,35 @@ RSpec.describe Labels::FindOrCreateService do project.add_developer(user) end + context 'when existing_labels_by_title is provided' do + let(:preloaded_label) { build(:label, title: 'Security') } + + before do + params.merge!( + existing_labels_by_title: { + 'Security' => preloaded_label + }) + end + + context 'when label exists' do + it 'returns preloaded label' do + expect(service.execute).to eq preloaded_label + end + end + + context 'when label does not exists' do + before do + params[:title] = 'Audit' + end + + it 'does not generates additional label search' do + service.execute + + expect(LabelsFinder).not_to receive(:new) + end + end + end + context 'when label does not exist at group level' do it 'creates a new label at project level' do expect { service.execute }.to change(project.labels, :count).by(1) diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb index 113bfb0f31a..076161c9029 100644 --- a/spec/services/merge_requests/update_assignees_service_spec.rb +++ b/spec/services/merge_requests/update_assignees_service_spec.rb @@ -36,6 +36,22 @@ RSpec.describe MergeRequests::UpdateAssigneesService do end context 'when the parameters are valid' do + context 'when using sentinel values' do + let(:opts) { { assignee_ids: [0] } } + + it 'removes all assignees' do + expect { update_merge_request }.to change(merge_request, :assignees).to([]) + end + end + + context 'the assignee_ids parameter is the empty list' do + let(:opts) { { assignee_ids: [] } } + + it 'removes all assignees' do + expect { update_merge_request }.to change(merge_request, :assignees).to([]) + end + end + it 'updates the MR, and queues the more expensive work for later' do expect_next(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service| expect(service) diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 266c8d5ee84..35dc709b5d9 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do let(:base_guest_permissions) do %i[ - award_emoji create_issue create_merge_request_in create_note + award_emoji create_issue create_incident create_merge_request_in create_note create_project read_issue_board read_issue read_issue_iid read_issue_link read_label read_issue_board_list read_milestone read_note read_project read_project_for_iids read_project_member read_release read_snippet diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 0a040557ffe..cfee26a0d6a 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -130,8 +130,8 @@ RSpec.shared_examples 'wiki controller actions' do it_behaves_like 'fetching history', :ok do let(:allow_read_wiki) { true } - it 'assigns @page_versions' do - expect(assigns(:page_versions)).to be_present + it 'assigns @commits' do + expect(assigns(:commits)).to be_present end end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index d05e5eb9120..013c9b61b99 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'project policies as anonymous' do context 'when a project has pending invites' do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji, :create_incident] } let(:anonymous_permissions) { guest_permissions - user_permissions } let(:current_user) { anonymous } diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index ef0bd97cbcf..6752bdc8337 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -62,12 +62,6 @@ RSpec.describe 'layouts/_head' do expect(rendered).to match('<link rel="stylesheet" media="print" href="/stylesheets/highlight/themes/solarised-light.css" />') end - it 'preloads Monaco' do - render - - expect(rendered).to match('<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">') - end - context 'when an asset_host is set and snowplow url is set' do let(:asset_host) { 'http://test.host' } let(:snowplow_collector_hostname) { 'www.snow.plow' } |