Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/assets/javascripts/boards/graphql/group_projects.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue1
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue1
-rw-r--r--app/controllers/concerns/wiki_actions.rb4
-rw-r--r--app/finders/packages/group_or_project_package_finder.rb45
-rw-r--r--app/finders/packages/maven/package_finder.rb33
-rw-r--r--app/finders/packages/nuget/package_finder.rb36
-rw-r--r--app/finders/packages/pypi/package_finder.rb17
-rw-r--r--app/finders/packages/pypi/packages_finder.rb20
-rw-r--r--app/graphql/queries/pipelines/get_pipeline_details.query.graphql1
-rw-r--r--app/graphql/resolvers/design_management/versions_resolver.rb1
-rw-r--r--app/graphql/types/ci/pipeline_type.rb3
-rw-r--r--app/graphql/types/design_management/version_type.rb4
-rw-r--r--app/helpers/webpack_helper.rb20
-rw-r--r--app/models/ci/job_artifact.rb16
-rw-r--r--app/models/ci/pipeline.rb3
-rw-r--r--app/models/design_management/version.rb1
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/wiki_page.rb13
-rw-r--r--app/policies/concerns/readonly_abilities.rb1
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/serializers/job_entity.rb1
-rw-r--r--app/services/ci/job_artifacts/destroy_associations_service.rb30
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb29
-rw-r--r--app/services/issues/base_service.rb7
-rw-r--r--app/services/issues/build_service.rb21
-rw-r--r--app/services/issues/update_service.rb1
-rw-r--r--app/services/labels/find_or_create_service.rb6
-rw-r--r--app/services/merge_requests/update_assignees_service.rb2
-rw-r--r--app/services/packages/maven/find_or_create_package_service.rb2
-rw-r--r--app/views/ide/_show.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/ci/pipeline_editor/show.html.haml2
-rw-r--r--app/views/shared/wikis/history.html.haml4
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--changelogs/unreleased/194104-part-2.yml5
-rw-r--r--changelogs/unreleased/224151-remove-job-artifacts-when-removing-a-pipeline.yml5
-rw-r--r--changelogs/unreleased/296547-enable-drawer-by-default.yml5
-rw-r--r--changelogs/unreleased/32081-design-rich-data.yml5
-rw-r--r--changelogs/unreleased/330369-unassign-all-assignees-from-merge-request-via-rest-api-not-possibl.yml5
-rw-r--r--changelogs/unreleased/add-complete-field-to-pipeline-and-job.yml5
-rw-r--r--changelogs/unreleased/dmishunov-monaco-tag.yml5
-rw-r--r--changelogs/unreleased/sy-restrict-issue-creation-to-appropriate-users.yml5
-rw-r--r--changelogs/unreleased/upgrade-pages-to-1-39-0.yml5
-rw-r--r--changelogs/unreleased/zj-port-page-versions-wiki.yml5
-rw-r--r--config/feature_flags/development/pipeline_editor_drawer.yml2
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/user/group/bulk_editing/img/bulk_editing_v13_11.pngbin66392 -> 0 bytes
-rw-r--r--doc/user/group/bulk_editing/index.md79
-rw-r--r--doc/user/group/epics/index.md2
-rw-r--r--doc/user/group/epics/manage_epics.md17
-rw-r--r--doc/user/project/issues/managing_issues.md25
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md115
-rw-r--r--doc/user/project/merge_requests/reviews/index.md20
-rw-r--r--lib/api/maven_packages.rb5
-rw-r--r--lib/api/pypi_packages.rb23
-rw-r--r--lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb11
-rw-r--r--lib/gitlab/git/wiki.rb16
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb24
-rw-r--r--locale/gitlab.pot27
-rw-r--r--spec/features/boards/new_issue_spec.rb93
-rw-r--r--spec/finders/packages/group_or_project_package_finder_spec.rb22
-rw-r--r--spec/finders/packages/maven/package_finder_spec.rb13
-rw-r--r--spec/finders/packages/pypi/package_finder_spec.rb45
-rw-r--r--spec/finders/packages/pypi/packages_finder_spec.rb70
-rw-r--r--spec/frontend/jobs/mock_data.js1
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap3
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js558
-rw-r--r--spec/graphql/resolvers/design_management/versions_resolver_spec.rb14
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/design_management/version_type_spec.rb2
-rw-r--r--spec/helpers/webpack_helper_spec.rb36
-rw-r--r--spec/models/ci/job_artifact_spec.rb28
-rw-r--r--spec/models/project_spec.rb26
-rw-r--r--spec/models/wiki_page_spec.rb17
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb17
-rw-r--r--spec/requests/api/merge_requests_spec.rb64
-rw-r--r--spec/serializers/job_entity_spec.rb4
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb7
-rw-r--r--spec/services/ci/job_artifacts/destroy_associations_service_spec.rb54
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb39
-rw-r--r--spec/services/issues/build_service_spec.rb6
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb29
-rw-r--r--spec/services/merge_requests/update_assignees_service_spec.rb16
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb2
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb6
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
deleted file mode 100644
index 4db2bbf264f..00000000000
--- a/doc/user/group/bulk_editing/img/bulk_editing_v13_11.png
+++ /dev/null
Binary files differ
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' }