summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.yml1
-rw-r--r--app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js (renamed from app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js)0
-rw-r--r--app/controllers/application_controller.rb10
-rw-r--r--app/controllers/concerns/confirm_email_warning.rb7
-rw-r--r--app/controllers/concerns/sourcegraph_gon.rb2
-rw-r--r--app/controllers/concerns/uploads_actions.rb19
-rw-r--r--app/controllers/instance_statistics/dev_ops_score_controller.rb (renamed from app/controllers/instance_statistics/conversational_development_index_controller.rb)2
-rw-r--r--app/controllers/projects/branches_controller.rb8
-rw-r--r--app/finders/clusters/knative_version_role_binding_finder.rb17
-rw-r--r--app/graphql/mutations/issues/set_confidential.rb27
-rw-r--r--app/graphql/mutations/todos/mark_done.rb7
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/ci/build.rb12
-rw-r--r--app/models/clusters/platforms/kubernetes.rb15
-rw-r--r--app/models/concerns/ci/contextable.rb11
-rw-r--r--app/models/dashboard_group_milestone.rb4
-rw-r--r--app/models/dashboard_milestone.rb4
-rw-r--r--app/models/group_milestone.rb4
-rw-r--r--app/models/milestone.rb10
-rw-r--r--app/models/project.rb8
-rw-r--r--app/services/branches/create_service.rb39
-rw-r--r--app/services/branches/delete_merged_service.rb34
-rw-r--r--app/services/branches/delete_service.rb32
-rw-r--r--app/services/branches/validate_new_service.rb31
-rw-r--r--app/services/clusters/kubernetes/create_or_update_service_account_service.rb41
-rw-r--r--app/services/clusters/kubernetes/kubernetes.rb3
-rw-r--r--app/services/commits/commit_patch_service.rb2
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/create_branch_service.rb38
-rw-r--r--app/services/delete_branch_service.rb30
-rw-r--r--app/services/delete_merged_branches_service.rb32
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb2
-rw-r--r--app/services/merge_requests/merge_service.rb2
-rw-r--r--app/services/todo_service.rb8
-rw-r--r--app/services/validate_new_branch_service.rb21
-rw-r--r--app/views/instance_statistics/dev_ops_score/_callout.html.haml (renamed from app/views/instance_statistics/conversational_development_index/_callout.html.haml)6
-rw-r--r--app/views/instance_statistics/dev_ops_score/_card.html.haml (renamed from app/views/instance_statistics/conversational_development_index/_card.html.haml)0
-rw-r--r--app/views/instance_statistics/dev_ops_score/_disabled.html.haml (renamed from app/views/instance_statistics/conversational_development_index/_disabled.html.haml)2
-rw-r--r--app/views/instance_statistics/dev_ops_score/_no_data.html.haml (renamed from app/views/instance_statistics/conversational_development_index/_no_data.html.haml)4
-rw-r--r--app/views/instance_statistics/dev_ops_score/index.html.haml (renamed from app/views/instance_statistics/conversational_development_index/index.html.haml)4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_instance_statistics.html.haml8
-rw-r--r--app/views/shared/icons/_dev_ops_score_no_data.svg (renamed from app/views/shared/icons/_convdev_no_data.svg)0
-rw-r--r--app/views/shared/icons/_dev_ops_score_no_index.svg (renamed from app/views/shared/icons/_convdev_no_index.svg)0
-rw-r--r--app/views/shared/icons/_dev_ops_score_overview.svg (renamed from app/views/shared/icons/_convdev_overview.svg)0
-rw-r--r--app/views/shared/milestones/_milestone.html.haml5
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml53
-rw-r--r--app/views/shared/milestones/_tabs.html.haml16
-rw-r--r--app/workers/delete_merged_branches_worker.rb2
-rw-r--r--changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml5
-rw-r--r--changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml5
-rw-r--r--changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml5
-rw-r--r--changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml5
-rw-r--r--changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml5
-rw-r--r--changelogs/unreleased/wolf-feat-milestone-hide-mr.yml5
-rw-r--r--config/routes/instance_statistics.rb4
-rw-r--r--config/webpack.config.js10
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql46
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json157
-rw-r--r--doc/api/graphql/reference/index.md8
-rw-r--r--doc/development/experiment_guide/index.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md2
-rw-r--r--doc/user/admin_area/license.md2
-rw-r--r--doc/user/project/clusters/serverless/index.md16
-rw-r--r--lib/api/branches.rb6
-rw-r--r--lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb14
-rw-r--r--lib/gitlab/git/commit.rb15
-rw-r--r--lib/gitlab/kubernetes/cluster_role.rb29
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb8
-rw-r--r--rubocop/cop/avoid_route_redirect_leading_slash.rb4
-rw-r--r--spec/controllers/application_controller_spec.rb14
-rw-r--r--spec/controllers/instance_statistics/dev_ops_score_controller_spec.rb (renamed from spec/controllers/instance_statistics/conversational_development_index_controller_spec.rb)2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb8
-rw-r--r--spec/controllers/uploads_controller_spec.rb24
-rw-r--r--spec/features/dashboard/milestones_spec.rb20
-rw-r--r--spec/features/instance_statistics/dev_ops_score_spec.rb (renamed from spec/features/instance_statistics/conversational_development_index_spec.rb)12
-rw-r--r--spec/features/merge_request/user_sees_deleted_target_branch_spec.rb2
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb8
-rw-r--r--spec/features/milestones/user_views_milestones_spec.rb31
-rw-r--r--spec/frontend/environment.js1
-rw-r--r--spec/frontend/monitoring/mock_data.js175
-rw-r--r--spec/frontend/sidebar/assignees_spec.js200
-rw-r--r--spec/frontend/sidebar/mock_data.js213
-rw-r--r--spec/graphql/mutations/issues/set_confidential_spec.rb39
-rw-r--r--spec/javascripts/monitoring/mock_data.js229
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js248
-rw-r--r--spec/javascripts/sidebar/mock_data.js214
-rw-r--r--spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb42
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb22
-rw-r--r--spec/models/ci/build_spec.rb74
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb20
-rw-r--r--spec/models/commit_spec.rb2
-rw-r--r--spec/models/milestone_spec.rb34
-rw-r--r--spec/models/project_spec.rb5
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb51
-rw-r--r--spec/requests/user_avatar_spec.rb36
-rw-r--r--spec/routing/instance_statistics_routing_spec.rb4
-rw-r--r--spec/services/branches/create_service_spec.rb (renamed from spec/services/create_branch_service_spec.rb)6
-rw-r--r--spec/services/branches/delete_merged_service_spec.rb (renamed from spec/services/delete_merged_branches_service_spec.rb)2
-rw-r--r--spec/services/branches/delete_service_spec.rb (renamed from spec/services/delete_branch_service_spec.rb)4
-rw-r--r--spec/services/branches/validate_new_service_spec.rb40
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb4
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb27
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb11
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/todo_service_spec.rb24
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb29
-rw-r--r--spec/support/helpers/position_tracer_helpers.rb2
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb10
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb4
114 files changed, 1884 insertions, 974 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index db03486e9fb..6366feaf897 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -31,7 +31,6 @@ rules:
- error
- allowElseIf: true
import/no-cycle: warn
- import/no-unresolved: warn
import/no-useless-path-segments: off
import/order: warn
lines-between-class-members: off
diff --git a/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
index c1056537f90..c1056537f90 100644
--- a/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js
+++ b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 25c1d80b117..4d55d7f00f0 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
- before_action :check_password_expiration
+ before_action :check_password_expiration, if: :html_request?
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
- before_action :add_gon_variables, unless: [:peek_request?, :json_request?]
+ before_action :add_gon_variables, if: :html_request?
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
before_action :active_user_check, unless: :devise_controller?
@@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
- def peek_request?
- request.path.start_with?('/-/peek')
+ def html_request?
+ request.format.html?
end
def json_request?
@@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base
def should_enforce_terms?
return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
- !(peek_request? || devise_controller?)
+ html_request? && !devise_controller?
end
def set_usage_stats_consent_flag
diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb
index 86df0010665..32e1a46e580 100644
--- a/app/controllers/concerns/confirm_email_warning.rb
+++ b/app/controllers/concerns/confirm_email_warning.rb
@@ -4,15 +4,18 @@ module ConfirmEmailWarning
extend ActiveSupport::Concern
included do
- before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) }
+ before_action :set_confirm_warning, if: :show_confirm_warning?
end
protected
+ def show_confirm_warning?
+ html_request? && request.get? && Feature.enabled?(:soft_email_confirmation)
+ end
+
def set_confirm_warning
return unless current_user
return if current_user.confirmed?
- return if peek_request? || json_request? || !request.get?
email = current_user.unconfirmed_email || current_user.email
diff --git a/app/controllers/concerns/sourcegraph_gon.rb b/app/controllers/concerns/sourcegraph_gon.rb
index ab4abd734fb..01925cf9d4d 100644
--- a/app/controllers/concerns/sourcegraph_gon.rb
+++ b/app/controllers/concerns/sourcegraph_gon.rb
@@ -4,7 +4,7 @@ module SourcegraphGon
extend ActiveSupport::Concern
included do
- before_action :push_sourcegraph_gon, unless: :json_request?
+ before_action :push_sourcegraph_gon, if: :html_request?
end
private
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index b87779c22d3..9b3b2c4a482 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true
module UploadsActions
+ extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
+ included do
+ prepend_before_action :set_request_format_from_path_extension
+ end
+
def create
uploader = UploadService.new(model, params[:file], uploader_class).execute
@@ -64,6 +69,20 @@ module UploadsActions
private
+ # Based on ActionDispatch::Http::MimeNegotiation. We have an
+ # initializer that monkey-patches this method out (so that repository
+ # paths don't guess a format based on extension), but we do want this
+ # behavior when serving uploads.
+ def set_request_format_from_path_extension
+ path = request.headers['action_dispatch.original_path'] || request.headers['PATH_INFO']
+
+ if match = path&.match(/\.(\w+)\z/)
+ format = Mime[match.captures.first]
+
+ request.format = format.symbol if format
+ end
+ end
+
def uploader_class
raise NotImplementedError
end
diff --git a/app/controllers/instance_statistics/conversational_development_index_controller.rb b/app/controllers/instance_statistics/dev_ops_score_controller.rb
index f34347b4d22..238f7fa7707 100644
--- a/app/controllers/instance_statistics/conversational_development_index_controller.rb
+++ b/app/controllers/instance_statistics/dev_ops_score_controller.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController
+class InstanceStatistics::DevOpsScoreController < InstanceStatistics::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
@metric = DevOpsScore::Metric.order(:created_at).last&.present
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index a908da08f57..09754409104 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -46,7 +46,7 @@ class Projects::BranchesController < Projects::ApplicationController
def diverging_commit_counts
respond_to do |format|
format.json do
- service = Branches::DivergingCommitCountsService.new(repository)
+ service = ::Branches::DivergingCommitCountsService.new(repository)
branches = BranchesFinder.new(repository, params.permit(names: [])).execute
Gitlab::GitalyClient.allow_n_plus_1_calls do
@@ -63,7 +63,7 @@ class Projects::BranchesController < Projects::ApplicationController
redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present?
- result = CreateBranchService.new(project, current_user)
+ result = ::Branches::CreateService.new(project, current_user)
.execute(branch_name, ref)
success = (result[:status] == :success)
@@ -102,7 +102,7 @@ class Projects::BranchesController < Projects::ApplicationController
def destroy
@branch_name = Addressable::URI.unescape(params[:id])
- result = DeleteBranchService.new(project, current_user).execute(@branch_name)
+ result = ::Branches::DeleteService.new(project, current_user).execute(@branch_name)
respond_to do |format|
format.html do
@@ -118,7 +118,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
def destroy_all_merged
- DeleteMergedBranchesService.new(@project, current_user).async_execute
+ ::Branches::DeleteMergedService.new(@project, current_user).async_execute
redirect_to project_branches_path(@project),
notice: _('Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.')
diff --git a/app/finders/clusters/knative_version_role_binding_finder.rb b/app/finders/clusters/knative_version_role_binding_finder.rb
new file mode 100644
index 00000000000..06ec5ea557f
--- /dev/null
+++ b/app/finders/clusters/knative_version_role_binding_finder.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Clusters
+ class KnativeVersionRoleBindingFinder
+ attr_reader :cluster
+
+ def initialize(cluster)
+ @cluster = cluster
+ end
+
+ def execute
+ cluster&.kubeclient&.get_cluster_role_bindings&.find do |resource|
+ resource.metadata.name == Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb
new file mode 100644
index 00000000000..0fff5518665
--- /dev/null
+++ b/app/graphql/mutations/issues/set_confidential.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class SetConfidential < Base
+ graphql_name 'IssueSetConfidential'
+
+ argument :confidential,
+ GraphQL::BOOLEAN_TYPE,
+ required: true,
+ description: 'Whether or not to set the issue as a confidential.'
+
+ def resolve(project_path:, iid:, confidential:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ project = issue.project
+
+ ::Issues::UpdateService.new(project, current_user, confidential: confidential)
+ .execute(issue)
+
+ {
+ issue: issue,
+ errors: issue.errors.full_messages
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb
index 5483708b5c6..d738e387c43 100644
--- a/app/graphql/mutations/todos/mark_done.rb
+++ b/app/graphql/mutations/todos/mark_done.rb
@@ -16,22 +16,21 @@ module Mutations
null: false,
description: 'The requested todo'
- # rubocop: disable CodeReuse/ActiveRecord
def resolve(id:)
todo = authorized_find!(id: id)
- mark_done(Todo.where(id: todo.id)) unless todo.done?
+
+ mark_done(todo)
{
todo: todo.reset,
errors: errors_on_object(todo)
}
end
- # rubocop: enable CodeReuse/ActiveRecord
private
def mark_done(todo)
- TodoService.new.mark_todos_as_done(todo, current_user)
+ TodoService.new.mark_todo_as_done(todo, current_user)
end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ecdbba477d7..e8f4ec06177 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -9,6 +9,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
+ mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index caa4478c848..1293b0d0f59 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -425,6 +425,18 @@ module Ci
end
end
+ def expanded_kubernetes_namespace
+ return unless has_environment?
+
+ namespace = options.dig(:environment, :kubernetes, :namespace)
+
+ if namespace.present?
+ strong_memoize(:expanded_kubernetes_namespace) do
+ ExpandVariables.expand(namespace, -> { simple_variables })
+ end
+ end
+ end
+
def has_environment?
environment.present?
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 314ef78757d..ae720065387 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -63,7 +63,7 @@ module Clusters
default_value_for :authorization_type, :rbac
- def predefined_variables(project:, environment_name:)
+ def predefined_variables(project:, environment_name:, kubernetes_namespace: nil)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'KUBE_URL', value: api_url)
@@ -74,15 +74,15 @@ module Clusters
end
if !cluster.managed? || cluster.management_project == project
- namespace = Gitlab::Kubernetes::DefaultNamespace.new(cluster, project: project).from_environment_name(environment_name)
+ namespace = kubernetes_namespace || default_namespace(project, environment_name: environment_name)
variables
.append(key: 'KUBE_TOKEN', value: token, public: false, masked: true)
.append(key: 'KUBE_NAMESPACE', value: namespace)
.append(key: 'KUBECONFIG', value: kubeconfig(namespace), public: false, file: true)
- elsif kubernetes_namespace = find_persisted_namespace(project, environment_name: environment_name)
- variables.concat(kubernetes_namespace.predefined_variables)
+ elsif persisted_namespace = find_persisted_namespace(project, environment_name: environment_name)
+ variables.concat(persisted_namespace.predefined_variables)
end
variables.concat(cluster.predefined_variables)
@@ -107,6 +107,13 @@ module Clusters
private
+ def default_namespace(project, environment_name:)
+ Gitlab::Kubernetes::DefaultNamespace.new(
+ cluster,
+ project: project
+ ).from_environment_name(environment_name)
+ end
+
def find_persisted_namespace(project, environment_name:)
Clusters::KubernetesNamespaceFinder.new(
cluster,
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index b65e9096d4e..5ff537a7837 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -15,7 +15,7 @@ module Ci
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runnable? && runner
- variables.concat(project.deployment_variables(environment: environment)) if environment
+ variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
variables.concat(secret_group_variables)
@@ -72,6 +72,15 @@ module Ci
end
end
+ def deployment_variables(environment:)
+ return [] unless environment
+
+ project.deployment_variables(
+ environment: environment,
+ kubernetes_namespace: expanded_kubernetes_namespace
+ )
+ end
+
def secret_group_variables
return [] unless project.group
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index cf6094682f3..48c09f4cd6b 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -22,4 +22,8 @@ class DashboardGroupMilestone < GlobalMilestone
def dashboard_milestone?
true
end
+
+ def merge_requests_enabled?
+ true
+ end
end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
index 9b377b70e5b..fd59b94b737 100644
--- a/app/models/dashboard_milestone.rb
+++ b/app/models/dashboard_milestone.rb
@@ -12,4 +12,8 @@ class DashboardMilestone < GlobalMilestone
def project_milestone?
true
end
+
+ def merge_requests_enabled?
+ project.merge_requests_enabled?
+ end
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index bfda603c3cb..87338512d99 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -41,4 +41,8 @@ class GroupMilestone < GlobalMilestone
def legacy_group_milestone?
true
end
+
+ def merge_requests_enabled?
+ true
+ end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d0be54eed02..d29eb62af7a 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -274,6 +274,16 @@ class Milestone < ApplicationRecord
project_id.present?
end
+ def merge_requests_enabled?
+ if group_milestone?
+ # Assume that groups have at least one project with merge requests enabled.
+ # Otherwise, we would need to load all of the projects from the database.
+ true
+ elsif project_milestone?
+ project&.merge_requests_enabled?
+ end
+ end
+
private
# Milestone titles must be unique across project milestones and group milestones
diff --git a/app/models/project.rb b/app/models/project.rb
index 14207c48f66..03a99577d5c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1986,12 +1986,16 @@ class Project < ApplicationRecord
end
end
- def deployment_variables(environment:)
+ def deployment_variables(environment:, kubernetes_namespace: nil)
platform = deployment_platform(environment: environment)
return [] unless platform.present?
- platform.predefined_variables(project: self, environment_name: environment)
+ platform.predefined_variables(
+ project: self,
+ environment_name: environment,
+ kubernetes_namespace: kubernetes_namespace
+ )
end
def auto_devops_variables
diff --git a/app/services/branches/create_service.rb b/app/services/branches/create_service.rb
new file mode 100644
index 00000000000..c8afd97e6bf
--- /dev/null
+++ b/app/services/branches/create_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Branches
+ class CreateService < BaseService
+ def execute(branch_name, ref, create_master_if_empty: true)
+ create_master_branch if create_master_if_empty && project.empty_repo?
+
+ result = ::Branches::ValidateNewService.new(project).execute(branch_name)
+
+ return result if result[:status] == :error
+
+ new_branch = repository.add_branch(current_user, branch_name, ref)
+
+ if new_branch
+ success(new_branch)
+ else
+ error("Invalid reference name: #{branch_name}")
+ end
+ rescue Gitlab::Git::PreReceiveError => ex
+ error(ex.message)
+ end
+
+ def success(branch)
+ super().merge(branch: branch)
+ end
+
+ private
+
+ def create_master_branch
+ project.repository.create_file(
+ current_user,
+ '/README.md',
+ '',
+ message: 'Add README.md',
+ branch_name: 'master'
+ )
+ end
+ end
+end
diff --git a/app/services/branches/delete_merged_service.rb b/app/services/branches/delete_merged_service.rb
new file mode 100644
index 00000000000..9fd5964bf94
--- /dev/null
+++ b/app/services/branches/delete_merged_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Branches
+ class DeleteMergedService < BaseService
+ def async_execute
+ DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
+
+ branches = project.repository.merged_branch_names
+ # Prevent deletion of branches relevant to open merge requests
+ branches -= merge_request_branch_names
+ # Prevent deletion of protected branches
+ branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }
+
+ branches.each do |branch|
+ ::Branches::DeleteService.new(project, current_user).execute(branch)
+ end
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def merge_request_branch_names
+ # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
+ source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch)
+ target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch)
+ (source_names + target_names).uniq
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/services/branches/delete_service.rb b/app/services/branches/delete_service.rb
new file mode 100644
index 00000000000..ca2b4556b58
--- /dev/null
+++ b/app/services/branches/delete_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Branches
+ class DeleteService < BaseService
+ def execute(branch_name)
+ repository = project.repository
+ branch = repository.find_branch(branch_name)
+
+ unless current_user.can?(:push_code, project)
+ return ServiceResponse.error(
+ message: 'You dont have push access to repo',
+ http_status: 405)
+ end
+
+ unless branch
+ return ServiceResponse.error(
+ message: 'No such branch',
+ http_status: 404)
+ end
+
+ if repository.rm_branch(current_user, branch_name)
+ ServiceResponse.success(message: 'Branch was deleted')
+ else
+ ServiceResponse.error(
+ message: 'Failed to remove branch',
+ http_status: 400)
+ end
+ rescue Gitlab::Git::PreReceiveError => ex
+ ServiceResponse.error(message: ex.message, http_status: 400)
+ end
+ end
+end
diff --git a/app/services/branches/validate_new_service.rb b/app/services/branches/validate_new_service.rb
new file mode 100644
index 00000000000..e45183d160f
--- /dev/null
+++ b/app/services/branches/validate_new_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Branches
+ class ValidateNewService < BaseService
+ def initialize(project)
+ @project = project
+ end
+
+ def execute(branch_name, force: false)
+ return error('Branch name is invalid') unless valid_name?(branch_name)
+
+ if branch_exist?(branch_name) && !force
+ return error('Branch already exists')
+ end
+
+ success
+ rescue Gitlab::Git::PreReceiveError => ex
+ error(ex.message)
+ end
+
+ private
+
+ def valid_name?(branch_name)
+ Gitlab::GitRefValidator.validate(branch_name)
+ end
+
+ def branch_exist?(branch_name)
+ project.repository.branch_exists?(branch_name)
+ end
+ end
+end
diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
index d798dcdcfd3..0fea398d234 100644
--- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
+++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
@@ -49,8 +49,14 @@ module Clusters
create_or_update_knative_serving_role
create_or_update_knative_serving_role_binding
+
create_or_update_crossplane_database_role
create_or_update_crossplane_database_role_binding
+
+ return unless knative_serving_namespace
+
+ create_or_update_knative_version_role
+ create_or_update_knative_version_role_binding
end
private
@@ -64,6 +70,12 @@ module Clusters
).ensure_exists!
end
+ def knative_serving_namespace
+ kubeclient.core_client.get_namespaces.find do |namespace|
+ namespace.metadata.name == Clusters::Kubernetes::KNATIVE_SERVING_NAMESPACE
+ end
+ end
+
def create_role_or_cluster_role_binding
if namespace_creator
kubeclient.create_or_update_role_binding(role_binding_resource)
@@ -88,6 +100,14 @@ module Clusters
kubeclient.update_role_binding(crossplane_database_role_binding_resource)
end
+ def create_or_update_knative_version_role
+ kubeclient.update_cluster_role(knative_version_role_resource)
+ end
+
+ def create_or_update_knative_version_role_binding
+ kubeclient.update_cluster_role_binding(knative_version_role_binding_resource)
+ end
+
def service_account_resource
Gitlab::Kubernetes::ServiceAccount.new(
service_account_name,
@@ -166,6 +186,27 @@ module Clusters
service_account_name: service_account_name
).generate
end
+
+ def knative_version_role_resource
+ Gitlab::Kubernetes::ClusterRole.new(
+ name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME,
+ rules: [{
+ apiGroups: %w(apps),
+ resources: %w(deployments),
+ verbs: %w(list get)
+ }]
+ ).generate
+ end
+
+ def knative_version_role_binding_resource
+ subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }]
+
+ Gitlab::Kubernetes::ClusterRoleBinding.new(
+ Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME,
+ Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME,
+ subjects
+ ).generate
+ end
end
end
end
diff --git a/app/services/clusters/kubernetes/kubernetes.rb b/app/services/clusters/kubernetes/kubernetes.rb
index d29519999b2..59cb1c4b3a9 100644
--- a/app/services/clusters/kubernetes/kubernetes.rb
+++ b/app/services/clusters/kubernetes/kubernetes.rb
@@ -12,5 +12,8 @@ module Clusters
GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding'
GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role'
GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding'
+ GITLAB_KNATIVE_VERSION_ROLE_NAME = 'gitlab-knative-version-role'
+ GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME = 'gitlab-knative-version-rolebinding'
+ KNATIVE_SERVING_NAMESPACE = 'knative-serving'
end
end
diff --git a/app/services/commits/commit_patch_service.rb b/app/services/commits/commit_patch_service.rb
index 49113c3c691..4fa6c30e901 100644
--- a/app/services/commits/commit_patch_service.rb
+++ b/app/services/commits/commit_patch_service.rb
@@ -32,7 +32,7 @@ module Commits
end
def prepare_branch!
- branch_result = CreateBranchService.new(project, current_user)
+ branch_result = ::Branches::CreateService.new(project, current_user)
.execute(@branch_name, @start_branch)
if branch_result[:status] != :success
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index b42494563b2..bd238605ac1 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -101,7 +101,7 @@ module Commits
end
def validate_new_branch_name!
- result = ValidateNewBranchService.new(project, current_user).execute(@branch_name, force: force?)
+ result = ::Branches::ValidateNewService.new(project).execute(@branch_name, force: force?)
if result[:status] == :error
raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}")
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
deleted file mode 100644
index d58cb0f9e2b..00000000000
--- a/app/services/create_branch_service.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-class CreateBranchService < BaseService
- def execute(branch_name, ref, create_master_if_empty: true)
- create_master_branch if create_master_if_empty && project.empty_repo?
-
- result = ValidateNewBranchService.new(project, current_user)
- .execute(branch_name)
-
- return result if result[:status] == :error
-
- new_branch = repository.add_branch(current_user, branch_name, ref)
-
- if new_branch
- success(new_branch)
- else
- error("Invalid reference name: #{branch_name}")
- end
- rescue Gitlab::Git::PreReceiveError => ex
- error(ex.message)
- end
-
- def success(branch)
- super().merge(branch: branch)
- end
-
- private
-
- def create_master_branch
- project.repository.create_file(
- current_user,
- '/README.md',
- '',
- message: 'Add README.md',
- branch_name: 'master'
- )
- end
-end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
deleted file mode 100644
index fd41ce54486..00000000000
--- a/app/services/delete_branch_service.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-class DeleteBranchService < BaseService
- def execute(branch_name)
- repository = project.repository
- branch = repository.find_branch(branch_name)
-
- unless current_user.can?(:push_code, project)
- return ServiceResponse.error(
- message: 'You dont have push access to repo',
- http_status: 405)
- end
-
- unless branch
- return ServiceResponse.error(
- message: 'No such branch',
- http_status: 404)
- end
-
- if repository.rm_branch(current_user, branch_name)
- ServiceResponse.success(message: 'Branch was deleted')
- else
- ServiceResponse.error(
- message: 'Failed to remove branch',
- http_status: 400)
- end
- rescue Gitlab::Git::PreReceiveError => ex
- ServiceResponse.error(message: ex.message, http_status: 400)
- end
-end
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
deleted file mode 100644
index 80de897e94b..00000000000
--- a/app/services/delete_merged_branches_service.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-class DeleteMergedBranchesService < BaseService
- def async_execute
- DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
- end
-
- def execute
- raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
-
- branches = project.repository.merged_branch_names
- # Prevent deletion of branches relevant to open merge requests
- branches -= merge_request_branch_names
- # Prevent deletion of protected branches
- branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }
-
- branches.each do |branch|
- DeleteBranchService.new(project, current_user).execute(branch)
- end
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def merge_request_branch_names
- # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
- source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch)
- target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch)
- (source_names + target_names).uniq
- end
- # rubocop: enable CodeReuse/ActiveRecord
-end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index 200a34cae04..95fb99d3e7a 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -19,7 +19,7 @@ module MergeRequests
return error('Not allowed to create merge request') unless can_create_merge_request?
return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
- result = CreateBranchService.new(target_project, current_user).execute(branch_name, ref)
+ result = ::Branches::CreateService.new(target_project, current_user).execute(branch_name, ref)
return result if result[:status] == :error
new_merge_request = create(merge_request)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 2eef3eed804..4a109fe4e16 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -99,7 +99,7 @@ module MergeRequests
log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch?
- DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
+ ::Branches::DeleteService.new(@merge_request.source_project, branch_deletion_user)
.execute(merge_request.source_branch)
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e7bcca1a38d..55f888d5664 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -179,6 +179,14 @@ class TodoService
mark_todos_as_done(todos, current_user)
end
+ def mark_todo_as_done(todo, current_user)
+ return if todo.done?
+
+ todo.update(state: :done)
+
+ current_user.update_todos_count_cache
+ end
+
# When user marks some todos as pending
def mark_todos_as_pending(todos, current_user)
update_todos_state(todos, current_user, :pending)
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
deleted file mode 100644
index 3f4a59e5cee..00000000000
--- a/app/services/validate_new_branch_service.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'base_service'
-
-class ValidateNewBranchService < BaseService
- def execute(branch_name, force: false)
- valid_branch = Gitlab::GitRefValidator.validate(branch_name)
-
- unless valid_branch
- return error('Branch name is invalid')
- end
-
- if project.repository.branch_exists?(branch_name) && !force
- return error('Branch already exists')
- end
-
- success
- rescue Gitlab::Git::PreReceiveError => ex
- error(ex.message)
- end
-end
diff --git a/app/views/instance_statistics/conversational_development_index/_callout.html.haml b/app/views/instance_statistics/dev_ops_score/_callout.html.haml
index 15e31fa3d9c..61f998424f2 100644
--- a/app/views/instance_statistics/conversational_development_index/_callout.html.haml
+++ b/app/views/instance_statistics/dev_ops_score/_callout.html.haml
@@ -1,5 +1,5 @@
.prepend-top-default
-.user-callout{ data: { uid: 'convdev_intro_callout_dismissed' } }
+.user-callout{ data: { uid: 'dev_ops_score_intro_callout_dismissed' } }
.bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => _('Dismiss ConvDev introduction') }
@@ -9,5 +9,5 @@
= _('Introducing Your Conversational Development Index')
%p
= _('Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.')
- .svg-container.devops
- = custom_icon('convdev_overview')
+ .svg-container.convdev
+ = custom_icon('dev_ops_score_overview')
diff --git a/app/views/instance_statistics/conversational_development_index/_card.html.haml b/app/views/instance_statistics/dev_ops_score/_card.html.haml
index c63bd96a175..c63bd96a175 100644
--- a/app/views/instance_statistics/conversational_development_index/_card.html.haml
+++ b/app/views/instance_statistics/dev_ops_score/_card.html.haml
diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/dev_ops_score/_disabled.html.haml
index ddcbdf6dd27..da27ea17b61 100644
--- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
+++ b/app/views/instance_statistics/dev_ops_score/_disabled.html.haml
@@ -1,6 +1,6 @@
.container.devops-empty
.col-sm-12.justify-content-center.text-center
- = custom_icon('convdev_no_index')
+ = custom_icon('dev_ops_score_no_index')
%h4= _('Usage ping is not enabled')
- if !current_user.admin?
%p
diff --git a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml b/app/views/instance_statistics/dev_ops_score/_no_data.html.haml
index 2031bced4fc..54598244039 100644
--- a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
+++ b/app/views/instance_statistics/dev_ops_score/_no_data.html.haml
@@ -1,7 +1,7 @@
.container.devops-empty
.col-sm-12.justify-content-center.text-center
- = custom_icon('convdev_no_data')
+ = custom_icon('dev_ops_score_no_data')
%h4= _('Data is still calculating...')
%p
= _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.')
- = link_to _('Learn more'), help_page_path('user/instance_statistics/convdev'), target: '_blank'
+ = link_to _('Learn more'), help_page_path('user/instance_statistics/dev_ops_score'), target: '_blank'
diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/dev_ops_score/index.html.haml
index f9a40152380..bd457f4740a 100644
--- a/app/views/instance_statistics/conversational_development_index/index.html.haml
+++ b/app/views/instance_statistics/dev_ops_score/index.html.haml
@@ -2,7 +2,7 @@
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container
- - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed')
+ - if usage_ping_enabled && show_callout?('dev_ops_score_intro_callout_dismissed')
= render 'callout'
.prepend-top-default
@@ -19,7 +19,7 @@
= _('index')
%br
= _('score')
- = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/convdev')
+ = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/dev_ops_score')
.devops-cards.board-card-container
- @metric.cards.each do |card|
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index d339751848b..9a839765286 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -48,7 +48,7 @@
%li.dropdown
= render_if_exists 'dashboard/nav_link_list'
- if can?(current_user, :read_instance_statistics)
- = nav_link(controller: [:conversational_development_index, :cohorts]) do
+ = nav_link(controller: [:dev_ops_score, :cohorts]) do
= link_to instance_statistics_root_path do
= _('Instance Statistics')
- if current_user.admin?
diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
index 57180f27146..6a5f727bb48 100644
--- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
+++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml
@@ -6,15 +6,15 @@
= sprite_icon('chart', size: 24)
.sidebar-context-title= _('Instance Statistics')
%ul.sidebar-top-level-items
- = nav_link(controller: :conversational_development_index) do
- = link_to instance_statistics_conversational_development_index_index_path do
+ = nav_link(controller: :dev_ops_score) do
+ = link_to instance_statistics_dev_ops_score_index_path do
.nav-icon-container
= sprite_icon('comment')
%span.nav-item-name
= _('ConvDev Index')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :conversational_development_index, html_options: { class: "fly-out-top-item" } ) do
- = link_to instance_statistics_conversational_development_index_index_path do
+ = nav_link(controller: :dev_ops_score, html_options: { class: "fly-out-top-item" } ) do
+ = link_to instance_statistics_dev_ops_score_index_path do
%strong.fly-out-top-item-name
= _('ConvDev Index')
diff --git a/app/views/shared/icons/_convdev_no_data.svg b/app/views/shared/icons/_dev_ops_score_no_data.svg
index ed32b2333e7..ed32b2333e7 100644
--- a/app/views/shared/icons/_convdev_no_data.svg
+++ b/app/views/shared/icons/_dev_ops_score_no_data.svg
diff --git a/app/views/shared/icons/_convdev_no_index.svg b/app/views/shared/icons/_dev_ops_score_no_index.svg
index 95c00e81d10..95c00e81d10 100644
--- a/app/views/shared/icons/_convdev_no_index.svg
+++ b/app/views/shared/icons/_dev_ops_score_no_index.svg
diff --git a/app/views/shared/icons/_convdev_overview.svg b/app/views/shared/icons/_dev_ops_score_overview.svg
index 2f31113bad7..2f31113bad7 100644
--- a/app/views/shared/icons/_convdev_overview.svg
+++ b/app/views/shared/icons/_dev_ops_score_overview.svg
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index b324f35c338..6e50b31fd71 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -43,8 +43,9 @@
.col-sm-4.milestone-progress
= milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
- &middot;
- = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path
+ - if milestone.merge_requests_enabled?
+ &middot;
+ = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path
.float-lg-right.light #{milestone.percent_complete(current_user)}% complete
.col-sm-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index b6656e6283c..fbbcc4f3e68 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -105,38 +105,39 @@
= render_if_exists 'shared/milestones/weight', milestone: milestone
- .block.merge-requests
- .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
- %strong
- = custom_icon('mr_bold')
- %span= milestone.merge_requests.count
- .title.hide-collapsed
- Merge requests
- %span.badge.badge-pill= milestone.merge_requests.count
- .value.hide-collapsed.bold
- - if !project || can?(current_user, :read_merge_request, project)
- %span.milestone-stat
- = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do
+ - if milestone.merge_requests_enabled?
+ .block.merge-requests
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ %strong
+ = custom_icon('mr_bold')
+ %span= milestone.merge_requests.count
+ .title.hide-collapsed
+ Merge requests
+ %span.badge.badge-pill= milestone.merge_requests.count
+ .value.hide-collapsed.bold
+ - if !project || can?(current_user, :read_merge_request, project)
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do
+ Open:
+ = milestone.merge_requests.opened.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do
+ Closed:
+ = milestone.merge_requests.closed.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do
+ Merged:
+ = milestone.merge_requests.merged.count
+ - else
+ %span.milestone-stat
Open:
= milestone.merge_requests.opened.count
- %span.milestone-stat
- = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do
+ %span.milestone-stat
Closed:
= milestone.merge_requests.closed.count
- %span.milestone-stat
- = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do
+ %span.milestone-stat
Merged:
= milestone.merge_requests.merged.count
- - else
- %span.milestone-stat
- Open:
- = milestone.merge_requests.opened.count
- %span.milestone-stat
- Closed:
- = milestone.merge_requests.closed.count
- %span.milestone-stat
- Merged:
- = milestone.merge_requests.merged.count
- if project
- recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index f718c5767d1..538ebe79641 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -6,10 +6,11 @@
= link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do
= _('Issues')
%span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
- %li.nav-item
- = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do
- = _('Merge Requests')
- %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
+ - if milestone.merge_requests_enabled?
+ %li.nav-item
+ = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do
+ = _('Merge Requests')
+ %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
%li.nav-item
= link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do
= _('Participants')
@@ -26,9 +27,10 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
- .tab-pane#tab-merge-requests
- -# loaded async
- = render "shared/milestones/tab_loading"
+ - if milestone.merge_requests_enabled?
+ .tab-pane#tab-merge-requests
+ -# loaded async
+ = render "shared/milestones/tab_loading"
.tab-pane#tab-participants
-# loaded async
= render "shared/milestones/tab_loading"
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index 44b3db30d0d..f3d86233c1b 100644
--- a/app/workers/delete_merged_branches_worker.rb
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -15,7 +15,7 @@ class DeleteMergedBranchesWorker
user = User.find(user_id)
begin
- DeleteMergedBranchesService.new(project, user).execute
+ ::Branches::DeleteMergedService.new(project, user).execute
rescue Gitlab::Access::AccessDeniedError
return
end
diff --git a/changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml b/changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml
new file mode 100644
index 00000000000..f743f780837
--- /dev/null
+++ b/changelogs/unreleased/27630-deploy-to-ci-specified-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Use CI configured namespace for deployments to unmanaged clusters
+merge_request: 20686
+author:
+type: added
diff --git a/changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml b/changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml
new file mode 100644
index 00000000000..5c54495d2ec
--- /dev/null
+++ b/changelogs/unreleased/36318-graphql-mutation-for-changing-confidential-status-of-an-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Add GraphQL mutation for setting an issue as confidential
+merge_request: 20785
+author:
+type: added
diff --git a/changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml b/changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml
new file mode 100644
index 00000000000..6a27ddeb0cd
--- /dev/null
+++ b/changelogs/unreleased/36717-container-repositories-can-not-be-replicated.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Container repositories can not be replicated when s3 is used
+merge_request: 21068
+author:
+type: fixed
diff --git a/changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml b/changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml
new file mode 100644
index 00000000000..3e8302ce845
--- /dev/null
+++ b/changelogs/unreleased/37385-respect-commit-timezones-from-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: Respect the timezone reported from Gitaly
+merge_request: 21066
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml b/changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml
new file mode 100644
index 00000000000..5baf993629e
--- /dev/null
+++ b/changelogs/unreleased/gitlabktl-17-add-rbac-permissions-for-knative-version.yml
@@ -0,0 +1,5 @@
+---
+title: Add rbac access to knative-serving namespace deployments to get knative version information
+merge_request: 20244
+author:
+type: changed
diff --git a/changelogs/unreleased/wolf-feat-milestone-hide-mr.yml b/changelogs/unreleased/wolf-feat-milestone-hide-mr.yml
new file mode 100644
index 00000000000..cae6f6a8894
--- /dev/null
+++ b/changelogs/unreleased/wolf-feat-milestone-hide-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Hide Merge Request information on milestones when MRs are disabled for project
+merge_request: 20985
+author: Wolfgang Faust
+type: changed
diff --git a/config/routes/instance_statistics.rb b/config/routes/instance_statistics.rb
index 1102ef6b017..967255d5b82 100644
--- a/config/routes/instance_statistics.rb
+++ b/config/routes/instance_statistics.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
namespace :instance_statistics do
- root to: redirect('-/instance_statistics/conversational_development_index')
+ root to: redirect('-/instance_statistics/dev_ops_score')
resources :cohorts, only: :index
- resources :conversational_development_index, only: :index
+ resources :dev_ops_score, only: :index
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 9c7a3f42c97..f69a799ebcf 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -102,6 +102,7 @@ const alias = {
if (IS_EE) {
Object.assign(alias, {
ee: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
+ ee_component: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
ee_empty_states: path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'),
ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'),
ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'),
@@ -283,16 +284,13 @@ module.exports = {
jQuery: 'jquery',
}),
- new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, function(resource) {
- if (Object.keys(module.exports.resolve.alias).indexOf('ee') >= 0) {
- resource.request = resource.request.replace(/^ee_component/, 'ee');
- } else {
+ !IS_EE &&
+ new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, resource => {
resource.request = path.join(
ROOT_PATH,
'app/assets/javascripts/vue_shared/components/empty_component.js',
);
- }
- }),
+ }),
new CopyWebpackPlugin([
{
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index d8902be92eb..fc3c16abdaa 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2519,6 +2519,51 @@ type IssuePermissions {
}
"""
+Autogenerated input type of IssueSetConfidential
+"""
+input IssueSetConfidentialInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Whether or not to set the issue as a confidential.
+ """
+ confidential: Boolean!
+
+ """
+ The iid of the issue to mutate
+ """
+ iid: String!
+
+ """
+ The project the issue to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of IssueSetConfidential
+"""
+type IssueSetConfidentialPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The issue after mutation
+ """
+ issue: Issue
+}
+
+"""
Autogenerated input type of IssueSetDueDate
"""
input IssueSetDueDateInput {
@@ -3556,6 +3601,7 @@ type Mutation {
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
+ issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index fba3dcca14d..09f91e0f02f 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -14113,6 +14113,33 @@
"deprecationReason": null
},
{
+ "name": "issueSetConfidential",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "IssueSetConfidentialInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "IssueSetConfidentialPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "issueSetDueDate",
"description": null,
"args": [
@@ -14987,6 +15014,136 @@
},
{
"kind": "OBJECT",
+ "name": "IssueSetConfidentialPayload",
+ "description": "Autogenerated return type of IssueSetConfidential",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": "The issue after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "IssueSetConfidentialInput",
+ "description": "Autogenerated input type of IssueSetConfidential",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the issue to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the issue to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "confidential",
+ "description": "Whether or not to set the issue as a confidential.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "IssueSetDueDatePayload",
"description": "Autogenerated return type of IssueSetDueDate",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4b71c9a8eaf..772648ec715 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -375,6 +375,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
+### IssueSetConfidentialPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `issue` | Issue | The issue after mutation |
+
### IssueSetDueDatePayload
| Name | Type | Description |
diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md
index 5155433c9ad..c7a80a07c61 100644
--- a/doc/development/experiment_guide/index.md
+++ b/doc/development/experiment_guide/index.md
@@ -55,7 +55,7 @@ The author then adds a comment to this piece of code and adds a link to the issu
end
```
-- [ ] Track necessery events. See the [event tracking guide](../event_tracking/index.md) for details.
+- [ ] Track necessary events. See the [event tracking guide](../event_tracking/index.md) for details.
- [ ] After the merge request is merged, use [`chatops`](../../ci/chatops/README.md) to enable the feature flag and start the experiment. For visibility, please run the command in the `#s_growth` channel:
```
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 236f175cee5..6a2cc5eeb6b 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -26,7 +26,7 @@ If you need to update an existing Karma test file (found in `spec/javascripts`),
need to migrate the whole spec to Jest. Simply updating the Karma spec to test your change
is fine. It is probably more appropriate to migrate to Jest in a separate merge request.
-If you need to create a new test file, we strongly recommend creating one in Jest. This will
+If you create a new test file, it needs to be created in Jest. This will
help support our migration and we think you'll love using Jest.
As always, please use discretion. Jest solves a lot of issues we experienced in Karma and
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index dbcf250bc57..fe8903a9f01 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -67,7 +67,7 @@ Omnibus installations should add this entry to `gitlab.rb`:
gitlab_rails['license_file'] = "/path/to/license/file"
```
-CAUTION:: **Caution:**
+CAUTION: **Caution:**
These methods will only add a license at the time of installation. Use the
admin area in the web ui to renew or upgrade licenses.
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index ffd7b0c0f2a..21a5d41bf86 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -116,7 +116,8 @@ You must do the following:
1. Ensure GitLab can manage Knative:
- For a non-GitLab managed cluster, ensure that the service account for the token
- provided can manage resources in the `serving.knative.dev` API group.
+ provided can manage resources in the `serving.knative.dev` API group. It will also
+ need list access to the deployments in the `knative-serving` namespace.
- For a GitLab managed cluster, if you added the cluster in [GitLab 12.1 or later](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/30235),
then GitLab will already have the required access and you can proceed to the next step.
@@ -153,6 +154,19 @@ You must do the following:
- delete
- patch
- watch
+ ---
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: ClusterRole
+ metadata:
+ name: gitlab-knative-version-role
+ rules:
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - list
+ - get
```
Then run the following command:
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 054242dca4c..ce3ee0d7e61 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -137,7 +137,7 @@ module API
post ':id/repository/branches' do
authorize_push_project
- result = CreateBranchService.new(user_project, current_user)
+ result = ::Branches::CreateService.new(user_project, current_user)
.execute(params[:branch], params[:ref])
if result[:status] == :success
@@ -162,7 +162,7 @@ module API
commit = user_project.repository.commit(branch.dereferenced_target)
destroy_conditionally!(commit, last_updated: commit.authored_date) do
- result = DeleteBranchService.new(user_project, current_user)
+ result = ::Branches::DeleteService.new(user_project, current_user)
.execute(params[:branch])
if result.error?
@@ -173,7 +173,7 @@ module API
desc 'Delete all merged branches'
delete ':id/repository/merged_branches' do
- DeleteMergedBranchesService.new(user_project, current_user).async_execute
+ ::Branches::DeleteMergedService.new(user_project, current_user).async_execute
accepted!
end
diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
index 9950e1dec55..b47238a3083 100644
--- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
+++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
@@ -8,7 +8,7 @@ module Gitlab
def unmet?
deployment_cluster.present? &&
deployment_cluster.managed? &&
- missing_namespace?
+ (missing_namespace? || missing_knative_version_role_binding?)
end
def complete!
@@ -23,6 +23,10 @@ module Gitlab
kubernetes_namespace.nil? || kubernetes_namespace.service_account_token.blank?
end
+ def missing_knative_version_role_binding?
+ knative_version_role_binding.nil?
+ end
+
def deployment_cluster
build.deployment&.cluster
end
@@ -31,6 +35,14 @@ module Gitlab
build.deployment.environment
end
+ def knative_version_role_binding
+ strong_memoize(:knative_version_role_binding) do
+ Clusters::KnativeVersionRoleBindingFinder.new(
+ deployment_cluster
+ ).execute
+ end
+ end
+
def kubernetes_namespace
strong_memoize(:kubernetes_namespace) do
Clusters::KubernetesNamespaceFinder.new(
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 6210223917b..b2dc9a8a3c8 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -370,15 +370,26 @@ module Gitlab
# subject from the message to make it clearer when there's one
# available but not the other.
@message = message_from_gitaly_body
- @authored_date = Time.at(commit.author.date.seconds).utc
+ @authored_date = init_date_from_gitaly(commit.author)
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
- @committed_date = Time.at(commit.committer.date.seconds).utc
+
+ @committed_date = init_date_from_gitaly(commit.committer)
@committer_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup
@parent_ids = Array(commit.parent_ids)
end
+ # Gitaly provides a UNIX timestamp in author.date.seconds, and a timezone
+ # offset in author.timezone. If the latter isn't present, assume UTC.
+ def init_date_from_gitaly(author)
+ if author.timezone.present?
+ Time.strptime("#{author.date.seconds} #{author.timezone}", '%s %z')
+ else
+ Time.at(author.date.seconds).utc
+ end
+ end
+
def serialize_keys
SERIALIZE_KEYS
end
diff --git a/lib/gitlab/kubernetes/cluster_role.rb b/lib/gitlab/kubernetes/cluster_role.rb
new file mode 100644
index 00000000000..4d40736a0b5
--- /dev/null
+++ b/lib/gitlab/kubernetes/cluster_role.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class ClusterRole
+ attr_reader :name, :rules
+
+ def initialize(name:, rules:)
+ @name = name
+ @rules = rules
+ end
+
+ def generate
+ ::Kubeclient::Resource.new(
+ metadata: metadata,
+ rules: rules
+ )
+ end
+
+ private
+
+ def metadata
+ {
+ name: name
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index 66c28a9b702..b23ca095414 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -56,6 +56,7 @@ module Gitlab
# group client
delegate :create_cluster_role_binding,
:get_cluster_role_binding,
+ :get_cluster_role_bindings,
:update_cluster_role_binding,
to: :rbac_client
@@ -68,6 +69,13 @@ module Gitlab
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
# group client
+ delegate :create_cluster_role,
+ :get_cluster_role,
+ :update_cluster_role,
+ to: :rbac_client
+
+ # RBAC methods delegates to the apis/rbac.authorization.k8s.io api
+ # group client
delegate :create_role_binding,
:get_role_binding,
:update_role_binding,
diff --git a/rubocop/cop/avoid_route_redirect_leading_slash.rb b/rubocop/cop/avoid_route_redirect_leading_slash.rb
index 261d151fb1b..d66e434dc9c 100644
--- a/rubocop/cop/avoid_route_redirect_leading_slash.rb
+++ b/rubocop/cop/avoid_route_redirect_leading_slash.rb
@@ -7,10 +7,10 @@ module RuboCop
#
# @example
# # bad
- # root to: redirect('/-/instance/statistics/conversational_development_index')
+ # root to: redirect('/-/instance/statistics/dev_ops_score')
#
# # good
- # root to: redirect('-/instance/statistics/conversational_development_index')
+ # root to: redirect('-/instance/statistics/dev_ops_score')
#
class AvoidRouteRedirectLeadingSlash < RuboCop::Cop::Cop
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 4a10e7b5325..04bbffc587f 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -90,14 +90,6 @@ describe ApplicationController do
let(:format) { :html }
it_behaves_like 'setting gon variables'
-
- context 'for peek requests' do
- before do
- request.path = '/-/peek'
- end
-
- it_behaves_like 'not setting gon variables'
- end
end
context 'with json format' do
@@ -105,6 +97,12 @@ describe ApplicationController do
it_behaves_like 'not setting gon variables'
end
+
+ context 'with atom format' do
+ let(:format) { :atom }
+
+ it_behaves_like 'not setting gon variables'
+ end
end
describe 'session expiration' do
diff --git a/spec/controllers/instance_statistics/conversational_development_index_controller_spec.rb b/spec/controllers/instance_statistics/dev_ops_score_controller_spec.rb
index 4935cb265bf..5825c6295f6 100644
--- a/spec/controllers/instance_statistics/conversational_development_index_controller_spec.rb
+++ b/spec/controllers/instance_statistics/dev_ops_score_controller_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-describe InstanceStatistics::ConversationalDevelopmentIndexController do
+describe InstanceStatistics::DevOpsScoreController do
it_behaves_like 'instance statistics availability'
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index affe0e0f970..4f8ab6a5def 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -178,7 +178,7 @@ describe Projects::BranchesController do
it 'redirects to newly created branch' do
result = { status: :success, branch: double(name: branch) }
- expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result)
expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
post :create,
@@ -200,7 +200,7 @@ describe Projects::BranchesController do
it 'redirects to autodeploy setup page' do
result = { status: :success, branch: double(name: branch) }
- expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result)
expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
post :create,
@@ -221,7 +221,7 @@ describe Projects::BranchesController do
create(:cluster, :provided_by_gcp, projects: [project])
- expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result)
expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
post :create,
@@ -459,7 +459,7 @@ describe Projects::BranchesController do
end
it 'starts worker to delete merged branches' do
- expect_any_instance_of(DeleteMergedBranchesService).to receive(:async_execute)
+ expect_any_instance_of(::Branches::DeleteMergedService).to receive(:async_execute)
destroy_all_merged
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 1bcf3bb106b..f35babc1b56 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -228,10 +228,10 @@ describe UploadsController do
user.block
end
- it "redirects to the sign in page" do
+ it "responds with status 401" do
get :show, params: { model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" }
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -320,10 +320,10 @@ describe UploadsController do
end
context "when not signed in" do
- it "redirects to the sign in page" do
+ it "responds with status 401" do
get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" }
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -343,10 +343,10 @@ describe UploadsController do
project.add_maintainer(user)
end
- it "redirects to the sign in page" do
+ it "responds with status 401" do
get :show, params: { model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" }
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -439,10 +439,10 @@ describe UploadsController do
user.block
end
- it "redirects to the sign in page" do
+ it "responds with status 401" do
get :show, params: { model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" }
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -526,10 +526,10 @@ describe UploadsController do
end
context "when not signed in" do
- it "redirects to the sign in page" do
+ it "responds with status 401" do
get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" }
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -549,10 +549,10 @@ describe UploadsController do
project.add_maintainer(user)
end
- it "redirects to the sign in page" do
+ it "responds with status 401" do
get :show, params: { model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" }
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to have_gitlab_http_status(401)
end
end
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index c21bc922de7..4ad19710d90 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -30,6 +30,7 @@ describe 'Dashboard > Milestones' do
expect(current_path).to eq dashboard_milestones_path
expect(page).to have_content(milestone.title)
expect(page).to have_content(group.name)
+ expect(first('.milestone')).to have_content('Merge Requests')
end
describe 'new milestones dropdown', :js do
@@ -46,4 +47,23 @@ describe 'Dashboard > Milestones' do
end
end
end
+
+ describe 'with merge requests disabled' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) }
+ let!(:milestone) { create(:milestone, project: project) }
+
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ visit dashboard_milestones_path
+ end
+
+ it 'does not see milestones' do
+ expect(current_path).to eq dashboard_milestones_path
+ expect(page).to have_content(milestone.title)
+ expect(first('.milestone')).to have_no_content('Merge Requests')
+ end
+ end
end
diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/dev_ops_score_spec.rb
index 6d05682fcd5..c9e6ab67267 100644
--- a/spec/features/instance_statistics/conversational_development_index_spec.rb
+++ b/spec/features/instance_statistics/dev_ops_score_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-describe 'Conversational Development Index' do
+describe 'Dev Ops Score' do
before do
sign_in(create(:admin))
end
it 'has dismissable intro callout', :js do
- visit instance_statistics_conversational_development_index_index_path
+ visit instance_statistics_dev_ops_score_index_path
expect(page).to have_content 'Introducing Your Conversational Development Index'
@@ -23,13 +23,13 @@ describe 'Conversational Development Index' do
end
it 'shows empty state' do
- visit instance_statistics_conversational_development_index_index_path
+ visit instance_statistics_dev_ops_score_index_path
expect(page).to have_content('Usage ping is not enabled')
end
it 'hides the intro callout' do
- visit instance_statistics_conversational_development_index_index_path
+ visit instance_statistics_dev_ops_score_index_path
expect(page).not_to have_content 'Introducing Your Conversational Development Index'
end
@@ -39,7 +39,7 @@ describe 'Conversational Development Index' do
it 'shows empty state' do
stub_application_setting(usage_ping_enabled: true)
- visit instance_statistics_conversational_development_index_index_path
+ visit instance_statistics_dev_ops_score_index_path
expect(page).to have_content('Data is still calculating')
end
@@ -50,7 +50,7 @@ describe 'Conversational Development Index' do
stub_application_setting(usage_ping_enabled: true)
create(:dev_ops_score_metric)
- visit instance_statistics_conversational_development_index_index_path
+ visit instance_statistics_dev_ops_score_index_path
expect(page).to have_content(
'Issues created per active user 1.2 You 9.3 Lead 13.3%'
diff --git a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
index 224261dec00..9ef6847f7f5 100644
--- a/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_deleted_target_branch_spec.rb
@@ -9,7 +9,7 @@ describe 'Merge request > User sees deleted target branch', :js do
before do
project.add_maintainer(user)
- DeleteBranchService.new(project, user).execute('feature')
+ ::Branches::DeleteService.new(project, user).execute('feature')
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index c42eb8560a4..22b2ea81b32 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -178,10 +178,11 @@ describe 'Merge request > User selects branches for new MR', :js do
end
context 'with special characters in branch names' do
+ let(:create_branch_service) { ::Branches::CreateService.new(project, user) }
+
it 'escapes quotes in branch names' do
special_branch_name = '"with-quotes"'
- CreateBranchService.new(project, user)
- .execute(special_branch_name, 'add-pdf-file')
+ create_branch_service.execute(special_branch_name, 'add-pdf-file')
visit project_new_merge_request_path(project)
select_source_branch(special_branch_name)
@@ -192,8 +193,7 @@ describe 'Merge request > User selects branches for new MR', :js do
it 'does not escape unicode in branch names' do
special_branch_name = 'ʕ•ᴥ•ʔ'
- CreateBranchService.new(project, user)
- .execute(special_branch_name, 'add-pdf-file')
+ create_branch_service.execute(special_branch_name, 'add-pdf-file')
visit project_new_merge_request_path(project)
select_source_branch(special_branch_name)
diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb
index 09378cab5e3..c91fe95aa77 100644
--- a/spec/features/milestones/user_views_milestones_spec.rb
+++ b/spec/features/milestones/user_views_milestones_spec.rb
@@ -18,6 +18,7 @@ describe "User views milestones" do
expect(page).to have_content(milestone.title)
.and have_content(milestone.expires_at)
.and have_content("Issues")
+ .and have_content("Merge Requests")
end
context "with issues" do
@@ -32,6 +33,7 @@ describe "User views milestones" do
.and have_selector("#tab-issues li.issuable-row", count: 2)
.and have_content(issue.title)
.and have_content(closed_issue.title)
+ .and have_selector("#tab-merge-requests")
end
end
@@ -62,3 +64,32 @@ describe "User views milestones" do
end
end
end
+
+describe "User views milestones with no MR" do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :merge_requests_disabled) }
+ set(:milestone) { create(:milestone, project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_milestones_path(project))
+ end
+
+ it "shows milestone" do
+ expect(page).to have_content(milestone.title)
+ .and have_content(milestone.expires_at)
+ .and have_content("Issues")
+ .and have_no_content("Merge Requests")
+ end
+
+ it "opens milestone" do
+ click_link(milestone.title)
+
+ expect(current_path).to eq(project_milestone_path(project, milestone))
+ expect(page).to have_content(milestone.title)
+ .and have_selector("#tab-issues")
+ .and have_no_selector("#tab-merge-requests")
+ end
+end
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 3c6553f3547..cd4fae60049 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -31,6 +31,7 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.gon = {
ee: IS_EE,
};
+ this.global.IS_EE = IS_EE;
this.rejectedPromises = [];
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 758e86235be..a184892773b 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -1,5 +1,10 @@
+// This import path needs to be relative for now because this mock data is used in
+// Karma specs too, where the helpers/test_constants alias can not be resolved
+import { TEST_HOST } from '../helpers/test_constants';
+
export const mockHost = 'http://test.host';
export const mockProjectDir = '/frontend-fixtures/environments-project';
+export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`;
export const anomalyDeploymentData = [
{
@@ -278,6 +283,49 @@ export const mockedQueryResultPayload = {
],
};
+export const mockedQueryResultPayloadCoresTotal = {
+ metricId: '13_system_metrics_kubernetes_container_cores_total',
+ result: [
+ {
+ metric: {},
+ values: [
+ [1563272065.589, '9.396484375'],
+ [1563272125.589, '9.333984375'],
+ [1563272185.589, '9.333984375'],
+ [1563272245.589, '9.333984375'],
+ [1563272305.589, '9.333984375'],
+ [1563272365.589, '9.333984375'],
+ [1563272425.589, '9.38671875'],
+ [1563272485.589, '9.333984375'],
+ [1563272545.589, '9.333984375'],
+ [1563272605.589, '9.333984375'],
+ [1563272665.589, '9.333984375'],
+ [1563272725.589, '9.333984375'],
+ [1563272785.589, '9.396484375'],
+ [1563272845.589, '9.333984375'],
+ [1563272905.589, '9.333984375'],
+ [1563272965.589, '9.3984375'],
+ [1563273025.589, '9.337890625'],
+ [1563273085.589, '9.34765625'],
+ [1563273145.589, '9.337890625'],
+ [1563273205.589, '9.337890625'],
+ [1563273265.589, '9.337890625'],
+ [1563273325.589, '9.337890625'],
+ [1563273385.589, '9.337890625'],
+ [1563273445.589, '9.337890625'],
+ [1563273505.589, '9.337890625'],
+ [1563273565.589, '9.337890625'],
+ [1563273625.589, '9.337890625'],
+ [1563273685.589, '9.337890625'],
+ [1563273745.589, '9.337890625'],
+ [1563273805.589, '9.337890625'],
+ [1563273865.589, '9.390625'],
+ [1563273925.589, '9.390625'],
+ ],
+ },
+ ],
+};
+
export const metricsGroupsAPIResponse = [
{
group: 'System metrics (Kubernetes)',
@@ -460,3 +508,130 @@ export const dashboardGitResponse = [
path: '.gitlab/dashboards/dashboard_2.yml',
},
];
+
+export const graphDataPrometheusQuery = {
+ title: 'Super Chart A2',
+ type: 'single-stat',
+ weight: 2,
+ metrics: [
+ {
+ id: 'metric_a1',
+ metricId: '2',
+ query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
+ unit: 'MB',
+ label: 'Total Consumption',
+ metric_id: 2,
+ prometheus_endpoint_path:
+ '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
+ result: [
+ {
+ metric: { job: 'prometheus' },
+ value: ['2019-06-26T21:03:20.881Z', 91],
+ },
+ ],
+ },
+ ],
+};
+
+export const graphDataPrometheusQueryRange = {
+ title: 'Super Chart A1',
+ type: 'area-chart',
+ weight: 2,
+ metrics: [
+ {
+ id: 'metric_a1',
+ metricId: '2',
+ query_range:
+ 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
+ unit: 'MB',
+ label: 'Total Consumption',
+ metric_id: 2,
+ prometheus_endpoint_path:
+ '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
+ result: [
+ {
+ metric: {},
+ values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']],
+ },
+ ],
+ },
+ ],
+};
+
+export const graphDataPrometheusQueryRangeMultiTrack = {
+ title: 'Super Chart A3',
+ type: 'heatmap',
+ weight: 3,
+ x_label: 'Status Code',
+ y_label: 'Time',
+ metrics: [
+ {
+ metricId: '1',
+ id: 'response_metrics_nginx_ingress_throughput_status_code',
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)',
+ unit: 'req / sec',
+ label: 'Status Code',
+ metric_id: 1,
+ prometheus_endpoint_path:
+ '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
+ result: [
+ {
+ metric: { status_code: '1xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 0],
+ ['2019-08-30T16:00:00.000Z', 2],
+ ['2019-08-30T17:00:00.000Z', 0],
+ ['2019-08-30T18:00:00.000Z', 0],
+ ['2019-08-30T19:00:00.000Z', 0],
+ ['2019-08-30T20:00:00.000Z', 3],
+ ],
+ },
+ {
+ metric: { status_code: '2xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 1],
+ ['2019-08-30T16:00:00.000Z', 3],
+ ['2019-08-30T17:00:00.000Z', 6],
+ ['2019-08-30T18:00:00.000Z', 10],
+ ['2019-08-30T19:00:00.000Z', 8],
+ ['2019-08-30T20:00:00.000Z', 6],
+ ],
+ },
+ {
+ metric: { status_code: '3xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 1],
+ ['2019-08-30T16:00:00.000Z', 2],
+ ['2019-08-30T17:00:00.000Z', 3],
+ ['2019-08-30T18:00:00.000Z', 3],
+ ['2019-08-30T19:00:00.000Z', 2],
+ ['2019-08-30T20:00:00.000Z', 1],
+ ],
+ },
+ {
+ metric: { status_code: '4xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 2],
+ ['2019-08-30T16:00:00.000Z', 0],
+ ['2019-08-30T17:00:00.000Z', 0],
+ ['2019-08-30T18:00:00.000Z', 2],
+ ['2019-08-30T19:00:00.000Z', 0],
+ ['2019-08-30T20:00:00.000Z', 2],
+ ],
+ },
+ {
+ metric: { status_code: '5xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 0],
+ ['2019-08-30T16:00:00.000Z', 1],
+ ['2019-08-30T17:00:00.000Z', 0],
+ ['2019-08-30T18:00:00.000Z', 0],
+ ['2019-08-30T19:00:00.000Z', 0],
+ ['2019-08-30T20:00:00.000Z', 2],
+ ],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
new file mode 100644
index 00000000000..14b6da10991
--- /dev/null
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -0,0 +1,200 @@
+import { mount } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import Assignee from '~/sidebar/components/assignees/assignees.vue';
+import UsersMock from './mock_data';
+import UsersMockHelper from '../helpers/user_mock_data_helper';
+
+describe('Assignee component', () => {
+ const getDefaultProps = () => ({
+ rootPath: 'http://localhost:3000',
+ users: [],
+ editable: false,
+ });
+ let wrapper;
+
+ const createWrapper = (propsData = getDefaultProps()) => {
+ wrapper = mount(Assignee, {
+ propsData,
+ sync: false,
+ attachToDocument: true,
+ });
+ };
+
+ const findComponentTextNoUsers = () => wrapper.find('.assign-yourself');
+ const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('No assignees/users', () => {
+ it('displays no assignee icon when collapsed', () => {
+ createWrapper();
+ const collapsedChildren = findCollapsedChildren();
+
+ expect(collapsedChildren.length).toBe(1);
+ expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None');
+ expect(collapsedChildren.at(0).classes()).toContain('fa', 'fa-user');
+ });
+
+ it('displays only "None" when no users are assigned and the issue is read-only', () => {
+ createWrapper();
+ const componentTextNoUsers = trimText(findComponentTextNoUsers().text());
+
+ expect(componentTextNoUsers).toBe('None');
+ expect(componentTextNoUsers).not.toContain('assign yourself');
+ });
+
+ it('displays only "None" when no users are assigned and the issue can be edited', () => {
+ createWrapper({
+ ...getDefaultProps(),
+ editable: true,
+ });
+ const componentTextNoUsers = trimText(findComponentTextNoUsers().text());
+
+ expect(componentTextNoUsers).toContain('None');
+ expect(componentTextNoUsers).toContain('assign yourself');
+ });
+
+ it('emits the assign-self event when "assign yourself" is clicked', () => {
+ createWrapper({
+ ...getDefaultProps(),
+ editable: true,
+ });
+
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper.find('.assign-yourself .btn-link').trigger('click');
+
+ expect(wrapper.emitted('assign-self')).toBeTruthy();
+ });
+ });
+
+ describe('One assignee/user', () => {
+ it('displays one assignee icon when collapsed', () => {
+ createWrapper({
+ ...getDefaultProps(),
+ users: [UsersMock.user],
+ });
+
+ const collapsedChildren = findCollapsedChildren();
+ const assignee = collapsedChildren.at(0);
+
+ expect(collapsedChildren.length).toBe(1);
+ expect(assignee.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar);
+ expect(assignee.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`);
+
+ expect(trimText(assignee.find('.author').text())).toBe(UsersMock.user.name);
+ });
+ });
+
+ describe('Two or more assignees/users', () => {
+ it('displays two assignee icons when collapsed', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ createWrapper({
+ ...getDefaultProps(),
+ users,
+ });
+
+ const collapsedChildren = findCollapsedChildren();
+
+ expect(collapsedChildren.length).toBe(2);
+
+ const first = collapsedChildren.at(0);
+
+ expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar);
+ expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
+
+ expect(trimText(first.find('.author').text())).toBe(users[0].name);
+
+ const second = collapsedChildren.at(1);
+
+ expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar);
+ expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
+
+ expect(trimText(second.find('.author').text())).toBe(users[1].name);
+ });
+
+ it('displays one assignee icon and counter when collapsed', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ createWrapper({
+ ...getDefaultProps(),
+ users,
+ });
+
+ const collapsedChildren = findCollapsedChildren();
+
+ expect(collapsedChildren.length).toBe(2);
+
+ const first = collapsedChildren.at(0);
+
+ expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar);
+ expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
+
+ expect(trimText(first.find('.author').text())).toBe(users[0].name);
+
+ const second = collapsedChildren.at(1);
+
+ expect(trimText(second.find('.avatar-counter').text())).toBe('+2');
+ });
+
+ it('Shows two assignees', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ createWrapper({
+ ...getDefaultProps(),
+ users,
+ editable: true,
+ });
+
+ expect(wrapper.findAll('.user-item').length).toBe(users.length);
+ expect(wrapper.find('.user-list-more').exists()).toBe(false);
+ });
+
+ it('shows sorted assignee where "can merge" users are sorted first', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
+ createWrapper({
+ ...getDefaultProps(),
+ users,
+ editable: true,
+ });
+
+ expect(wrapper.vm.sortedAssigness[0].can_merge).toBe(true);
+ });
+
+ it('passes the sorted assignees to the uncollapsed-assignee-list', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
+ createWrapper({
+ ...getDefaultProps(),
+ users,
+ });
+
+ const userItems = wrapper.findAll('.user-list .user-item a');
+
+ expect(userItems.length).toBe(3);
+ expect(userItems.at(0).attributes('data-original-title')).toBe(users[2].name);
+ });
+
+ it('passes the sorted assignees to the collapsed-assignee-list', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
+ createWrapper({
+ ...getDefaultProps(),
+ users,
+ });
+
+ const collapsedButton = wrapper.find('.sidebar-collapsed-user button');
+
+ expect(trimText(collapsedButton.text())).toBe(users[2].name);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
new file mode 100644
index 00000000000..3ee97b978fd
--- /dev/null
+++ b/spec/frontend/sidebar/mock_data.js
@@ -0,0 +1,213 @@
+const RESPONSE_MAP = {
+ GET: {
+ '/gitlab-org/gitlab-shell/issues/5.json': {
+ id: 45,
+ iid: 5,
+ author_id: 23,
+ description: 'Nulla ullam commodi delectus adipisci quis sit.',
+ lock_version: null,
+ milestone_id: 21,
+ position: 0,
+ state: 'closed',
+ title: 'Vel et nulla voluptatibus corporis dolor iste saepe laborum.',
+ updated_by_id: 1,
+ created_at: '2017-02-02T21: 49: 49.664Z',
+ updated_at: '2017-05-03T22: 26: 03.760Z',
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ branch_name: null,
+ confidential: false,
+ assignees: [
+ {
+ name: 'User 0',
+ username: 'user0',
+ id: 22,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http: //localhost:3001/user0',
+ },
+ {
+ name: 'Marguerite Bartell',
+ username: 'tajuana',
+ id: 18,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http: //localhost:3001/tajuana',
+ },
+ {
+ name: 'Laureen Ritchie',
+ username: 'michaele.will',
+ id: 16,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http: //localhost:3001/michaele.will',
+ },
+ ],
+ due_date: null,
+ moved_to_id: null,
+ project_id: 4,
+ weight: null,
+ milestone: {
+ id: 21,
+ iid: 1,
+ project_id: 4,
+ title: 'v0.0',
+ description: 'Molestiae commodi laboriosam odio sunt eaque reprehenderit.',
+ state: 'active',
+ created_at: '2017-02-02T21: 49: 30.530Z',
+ updated_at: '2017-02-02T21: 49: 30.530Z',
+ due_date: null,
+ start_date: null,
+ },
+ labels: [],
+ },
+ '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras': {
+ assignees: [
+ {
+ name: 'User 0',
+ username: 'user0',
+ id: 22,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/user0',
+ },
+ {
+ name: 'Marguerite Bartell',
+ username: 'tajuana',
+ id: 18,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/tajuana',
+ },
+ {
+ name: 'Laureen Ritchie',
+ username: 'michaele.will',
+ id: 16,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/michaele.will',
+ },
+ ],
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ participants: [
+ {
+ name: 'User 0',
+ username: 'user0',
+ id: 22,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/user0',
+ },
+ {
+ name: 'Marguerite Bartell',
+ username: 'tajuana',
+ id: 18,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/tajuana',
+ },
+ {
+ name: 'Laureen Ritchie',
+ username: 'michaele.will',
+ id: 16,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/michaele.will',
+ },
+ ],
+ subscribed: true,
+ time_estimate: 0,
+ total_time_spent: 0,
+ },
+ '/autocomplete/projects?project_id=15': [
+ {
+ id: 0,
+ name_with_namespace: 'No project',
+ },
+ {
+ id: 20,
+ name_with_namespace: '<img src=x onerror=alert(document.domain)> foo / bar',
+ },
+ ],
+ },
+ PUT: {
+ '/gitlab-org/gitlab-shell/issues/5.json': {
+ data: {},
+ },
+ },
+ POST: {
+ '/gitlab-org/gitlab-shell/issues/5/move': {
+ id: 123,
+ iid: 5,
+ author_id: 1,
+ description: 'some description',
+ lock_version: 5,
+ milestone_id: null,
+ state: 'opened',
+ title: 'some title',
+ updated_by_id: 1,
+ created_at: '2017-06-27T19:54:42.437Z',
+ updated_at: '2017-08-18T03:39:49.222Z',
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ branch_name: null,
+ confidential: false,
+ assignees: [],
+ due_date: null,
+ moved_to_id: null,
+ project_id: 7,
+ milestone: null,
+ labels: [],
+ web_url: '/root/some-project/issues/5',
+ },
+ '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
+ },
+};
+
+const mockData = {
+ responseMap: RESPONSE_MAP,
+ mediator: {
+ endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras',
+ toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
+ moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
+ projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
+ editable: true,
+ currentUser: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ rootPath: '/',
+ fullPath: '/gitlab-org/gitlab-shell',
+ },
+ time: {
+ time_estimate: 3600,
+ total_time_spent: 0,
+ human_time_estimate: '1h',
+ human_total_time_spent: null,
+ },
+ user: {
+ avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+};
+
+export default mockData;
diff --git a/spec/graphql/mutations/issues/set_confidential_spec.rb b/spec/graphql/mutations/issues/set_confidential_spec.rb
new file mode 100644
index 00000000000..05b787eb5ca
--- /dev/null
+++ b/spec/graphql/mutations/issues/set_confidential_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::Issues::SetConfidential do
+ let(:issue) { create(:issue) }
+ let(:user) { create(:user) }
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
+
+ describe '#resolve' do
+ let(:confidential) { true }
+ let(:mutated_issue) { subject[:issue] }
+ subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, confidential: confidential) }
+
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+
+ context 'when the user can update the issue' do
+ before do
+ issue.project.add_developer(user)
+ end
+
+ it 'returns the issue as confidential' do
+ expect(mutated_issue).to eq(issue)
+ expect(mutated_issue.confidential).to be_truthy
+ expect(subject[:errors]).to be_empty
+ end
+
+ context 'when passing confidential as false' do
+ let(:confidential) { false }
+
+ it 'updates the issue confidentiality to false' do
+ expect(mutated_issue.confidential).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index f59c4ee4264..c80401e8c1d 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,226 +1,5 @@
-import {
- anomalyMockGraphData as importedAnomalyMockGraphData,
- metricsGroupsAPIResponse as importedMetricsGroupsAPIResponse,
- environmentData as importedEnvironmentData,
- dashboardGitResponse as importedDashboardGitResponse,
-} from '../../frontend/monitoring/mock_data';
+// No new code should be added to this file. Instead, modify the
+// file this one re-exports from. For more detail about why, see:
+// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-export const anomalyMockGraphData = importedAnomalyMockGraphData;
-export const metricsGroupsAPIResponse = importedMetricsGroupsAPIResponse;
-export const environmentData = importedEnvironmentData;
-export const dashboardGitResponse = importedDashboardGitResponse;
-
-export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
-
-export const mockedQueryResultPayload = {
- metricId: '17_system_metrics_kubernetes_container_memory_average',
- result: [
- {
- metric: {},
- values: [
- [1563272065.589, '10.396484375'],
- [1563272125.589, '10.333984375'],
- [1563272185.589, '10.333984375'],
- [1563272245.589, '10.333984375'],
- [1563272305.589, '10.333984375'],
- [1563272365.589, '10.333984375'],
- [1563272425.589, '10.38671875'],
- [1563272485.589, '10.333984375'],
- [1563272545.589, '10.333984375'],
- [1563272605.589, '10.333984375'],
- [1563272665.589, '10.333984375'],
- [1563272725.589, '10.333984375'],
- [1563272785.589, '10.396484375'],
- [1563272845.589, '10.333984375'],
- [1563272905.589, '10.333984375'],
- [1563272965.589, '10.3984375'],
- [1563273025.589, '10.337890625'],
- [1563273085.589, '10.34765625'],
- [1563273145.589, '10.337890625'],
- [1563273205.589, '10.337890625'],
- [1563273265.589, '10.337890625'],
- [1563273325.589, '10.337890625'],
- [1563273385.589, '10.337890625'],
- [1563273445.589, '10.337890625'],
- [1563273505.589, '10.337890625'],
- [1563273565.589, '10.337890625'],
- [1563273625.589, '10.337890625'],
- [1563273685.589, '10.337890625'],
- [1563273745.589, '10.337890625'],
- [1563273805.589, '10.337890625'],
- [1563273865.589, '10.390625'],
- [1563273925.589, '10.390625'],
- ],
- },
- ],
-};
-
-export const mockedQueryResultPayloadCoresTotal = {
- metricId: '13_system_metrics_kubernetes_container_cores_total',
- result: [
- {
- metric: {},
- values: [
- [1563272065.589, '9.396484375'],
- [1563272125.589, '9.333984375'],
- [1563272185.589, '9.333984375'],
- [1563272245.589, '9.333984375'],
- [1563272305.589, '9.333984375'],
- [1563272365.589, '9.333984375'],
- [1563272425.589, '9.38671875'],
- [1563272485.589, '9.333984375'],
- [1563272545.589, '9.333984375'],
- [1563272605.589, '9.333984375'],
- [1563272665.589, '9.333984375'],
- [1563272725.589, '9.333984375'],
- [1563272785.589, '9.396484375'],
- [1563272845.589, '9.333984375'],
- [1563272905.589, '9.333984375'],
- [1563272965.589, '9.3984375'],
- [1563273025.589, '9.337890625'],
- [1563273085.589, '9.34765625'],
- [1563273145.589, '9.337890625'],
- [1563273205.589, '9.337890625'],
- [1563273265.589, '9.337890625'],
- [1563273325.589, '9.337890625'],
- [1563273385.589, '9.337890625'],
- [1563273445.589, '9.337890625'],
- [1563273505.589, '9.337890625'],
- [1563273565.589, '9.337890625'],
- [1563273625.589, '9.337890625'],
- [1563273685.589, '9.337890625'],
- [1563273745.589, '9.337890625'],
- [1563273805.589, '9.337890625'],
- [1563273865.589, '9.390625'],
- [1563273925.589, '9.390625'],
- ],
- },
- ],
-};
-
-export const graphDataPrometheusQuery = {
- title: 'Super Chart A2',
- type: 'single-stat',
- weight: 2,
- metrics: [
- {
- id: 'metric_a1',
- metricId: '2',
- query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
- unit: 'MB',
- label: 'Total Consumption',
- metric_id: 2,
- prometheus_endpoint_path:
- '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
- result: [
- {
- metric: { job: 'prometheus' },
- value: ['2019-06-26T21:03:20.881Z', 91],
- },
- ],
- },
- ],
-};
-
-export const graphDataPrometheusQueryRange = {
- title: 'Super Chart A1',
- type: 'area-chart',
- weight: 2,
- metrics: [
- {
- id: 'metric_a1',
- metricId: '2',
- query_range:
- 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
- unit: 'MB',
- label: 'Total Consumption',
- metric_id: 2,
- prometheus_endpoint_path:
- '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
- result: [
- {
- metric: {},
- values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']],
- },
- ],
- },
- ],
-};
-
-export const graphDataPrometheusQueryRangeMultiTrack = {
- title: 'Super Chart A3',
- type: 'heatmap',
- weight: 3,
- x_label: 'Status Code',
- y_label: 'Time',
- metrics: [
- {
- metricId: '1',
- id: 'response_metrics_nginx_ingress_throughput_status_code',
- query_range:
- 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)',
- unit: 'req / sec',
- label: 'Status Code',
- metric_id: 1,
- prometheus_endpoint_path:
- '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
- result: [
- {
- metric: { status_code: '1xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 0],
- ['2019-08-30T16:00:00.000Z', 2],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 0],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 3],
- ],
- },
- {
- metric: { status_code: '2xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 1],
- ['2019-08-30T16:00:00.000Z', 3],
- ['2019-08-30T17:00:00.000Z', 6],
- ['2019-08-30T18:00:00.000Z', 10],
- ['2019-08-30T19:00:00.000Z', 8],
- ['2019-08-30T20:00:00.000Z', 6],
- ],
- },
- {
- metric: { status_code: '3xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 1],
- ['2019-08-30T16:00:00.000Z', 2],
- ['2019-08-30T17:00:00.000Z', 3],
- ['2019-08-30T18:00:00.000Z', 3],
- ['2019-08-30T19:00:00.000Z', 2],
- ['2019-08-30T20:00:00.000Z', 1],
- ],
- },
- {
- metric: { status_code: '4xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 2],
- ['2019-08-30T16:00:00.000Z', 0],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 2],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 2],
- ],
- },
- {
- metric: { status_code: '5xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 0],
- ['2019-08-30T16:00:00.000Z', 1],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 0],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 2],
- ],
- },
- ],
- },
- ],
-};
+export * from '../../frontend/monitoring/mock_data';
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
deleted file mode 100644
index a1df5389a38..00000000000
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ /dev/null
@@ -1,248 +0,0 @@
-import Vue from 'vue';
-import Assignee from '~/sidebar/components/assignees/assignees.vue';
-import UsersMock from './mock_data';
-import UsersMockHelper from '../helpers/user_mock_data_helper';
-
-describe('Assignee component', () => {
- let component;
- let AssigneeComponent;
-
- beforeEach(() => {
- AssigneeComponent = Vue.extend(Assignee);
- });
-
- describe('No assignees/users', () => {
- it('displays no assignee icon when collapsed', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users: [],
- editable: false,
- },
- }).$mount();
-
- const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
-
- expect(collapsed.childElementCount).toEqual(1);
- expect(collapsed.children[0].getAttribute('aria-label')).toEqual('None');
- expect(collapsed.children[0].classList.contains('fa')).toEqual(true);
- expect(collapsed.children[0].classList.contains('fa-user')).toEqual(true);
- });
-
- it('displays only "None" when no users are assigned and the issue is read-only', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users: [],
- editable: false,
- },
- }).$mount();
- const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
-
- expect(componentTextNoUsers).toBe('None');
- expect(componentTextNoUsers.indexOf('assign yourself')).toEqual(-1);
- });
-
- it('displays only "None" when no users are assigned and the issue can be edited', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users: [],
- editable: true,
- },
- }).$mount();
- const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
-
- expect(componentTextNoUsers.indexOf('None')).toEqual(0);
- expect(componentTextNoUsers.indexOf('assign yourself')).toBeGreaterThan(0);
- });
-
- it('emits the assign-self event when "assign yourself" is clicked', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users: [],
- editable: true,
- },
- }).$mount();
-
- spyOn(component, '$emit');
- component.$el.querySelector('.assign-yourself .btn-link').click();
-
- expect(component.$emit).toHaveBeenCalledWith('assign-self');
- });
- });
-
- describe('One assignee/user', () => {
- it('displays one assignee icon when collapsed', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users: [UsersMock.user],
- editable: false,
- },
- }).$mount();
-
- const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
- const assignee = collapsed.children[0];
-
- expect(collapsed.childElementCount).toEqual(1);
- expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar);
- expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(
- `${UsersMock.user.name}'s avatar`,
- );
-
- expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
- });
- });
-
- describe('Two or more assignees/users', () => {
- it('has no "cannot merge" tooltip when every user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
- users[0].can_merge = true;
- users[1].can_merge = true;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.collapsedTooltipTitle).not.toContain('cannot merge');
- });
-
- it('displays two assignee icons when collapsed', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: false,
- },
- }).$mount();
-
- const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
-
- expect(collapsed.childElementCount).toEqual(2);
-
- const first = collapsed.children[0];
-
- expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
- expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
- `${users[0].name}'s avatar`,
- );
-
- expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
-
- const second = collapsed.children[1];
-
- expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar);
- expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(
- `${users[1].name}'s avatar`,
- );
-
- expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name);
- });
-
- it('displays one assignee icon and counter when collapsed', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: false,
- },
- }).$mount();
-
- const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
-
- expect(collapsed.childElementCount).toEqual(2);
-
- const first = collapsed.children[0];
-
- expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
- expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
- `${users[0].name}'s avatar`,
- );
-
- expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
-
- const second = collapsed.children[1];
-
- expect(second.querySelector('.avatar-counter').innerText.trim()).toEqual('+2');
- });
-
- it('Shows two assignees', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: true,
- },
- }).$mount();
-
- expect(component.$el.querySelectorAll('.user-item').length).toEqual(users.length);
- expect(component.$el.querySelector('.user-list-more')).toBe(null);
- });
-
- it('shows sorted assignee where "can merge" users are sorted first', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = false;
- users[2].can_merge = true;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: true,
- },
- }).$mount();
-
- expect(component.sortedAssigness[0].can_merge).toBe(true);
- });
-
- it('passes the sorted assignees to the uncollapsed-assignee-list', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = false;
- users[2].can_merge = true;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: false,
- },
- }).$mount();
-
- const userItems = component.$el.querySelectorAll('.user-list .user-item a');
-
- expect(userItems.length).toBe(3);
- expect(userItems[0].dataset.originalTitle).toBe(users[2].name);
- });
-
- it('passes the sorted assignees to the collapsed-assignee-list', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = false;
- users[2].can_merge = true;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: false,
- },
- }).$mount();
-
- const collapsedButton = component.$el.querySelector('.sidebar-collapsed-user button');
-
- expect(collapsedButton.innerText.trim()).toBe(users[2].name);
- });
- });
-});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 3ee97b978fd..c869ff96933 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -1,213 +1,7 @@
-const RESPONSE_MAP = {
- GET: {
- '/gitlab-org/gitlab-shell/issues/5.json': {
- id: 45,
- iid: 5,
- author_id: 23,
- description: 'Nulla ullam commodi delectus adipisci quis sit.',
- lock_version: null,
- milestone_id: 21,
- position: 0,
- state: 'closed',
- title: 'Vel et nulla voluptatibus corporis dolor iste saepe laborum.',
- updated_by_id: 1,
- created_at: '2017-02-02T21: 49: 49.664Z',
- updated_at: '2017-05-03T22: 26: 03.760Z',
- time_estimate: 0,
- total_time_spent: 0,
- human_time_estimate: null,
- human_total_time_spent: null,
- branch_name: null,
- confidential: false,
- assignees: [
- {
- name: 'User 0',
- username: 'user0',
- id: 22,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/user0',
- },
- {
- name: 'Marguerite Bartell',
- username: 'tajuana',
- id: 18,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/tajuana',
- },
- {
- name: 'Laureen Ritchie',
- username: 'michaele.will',
- id: 16,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/michaele.will',
- },
- ],
- due_date: null,
- moved_to_id: null,
- project_id: 4,
- weight: null,
- milestone: {
- id: 21,
- iid: 1,
- project_id: 4,
- title: 'v0.0',
- description: 'Molestiae commodi laboriosam odio sunt eaque reprehenderit.',
- state: 'active',
- created_at: '2017-02-02T21: 49: 30.530Z',
- updated_at: '2017-02-02T21: 49: 30.530Z',
- due_date: null,
- start_date: null,
- },
- labels: [],
- },
- '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras': {
- assignees: [
- {
- name: 'User 0',
- username: 'user0',
- id: 22,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http://localhost:3001/user0',
- },
- {
- name: 'Marguerite Bartell',
- username: 'tajuana',
- id: 18,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http://localhost:3001/tajuana',
- },
- {
- name: 'Laureen Ritchie',
- username: 'michaele.will',
- id: 16,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http://localhost:3001/michaele.will',
- },
- ],
- human_time_estimate: null,
- human_total_time_spent: null,
- participants: [
- {
- name: 'User 0',
- username: 'user0',
- id: 22,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http://localhost:3001/user0',
- },
- {
- name: 'Marguerite Bartell',
- username: 'tajuana',
- id: 18,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http://localhost:3001/tajuana',
- },
- {
- name: 'Laureen Ritchie',
- username: 'michaele.will',
- id: 16,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http://localhost:3001/michaele.will',
- },
- ],
- subscribed: true,
- time_estimate: 0,
- total_time_spent: 0,
- },
- '/autocomplete/projects?project_id=15': [
- {
- id: 0,
- name_with_namespace: 'No project',
- },
- {
- id: 20,
- name_with_namespace: '<img src=x onerror=alert(document.domain)> foo / bar',
- },
- ],
- },
- PUT: {
- '/gitlab-org/gitlab-shell/issues/5.json': {
- data: {},
- },
- },
- POST: {
- '/gitlab-org/gitlab-shell/issues/5/move': {
- id: 123,
- iid: 5,
- author_id: 1,
- description: 'some description',
- lock_version: 5,
- milestone_id: null,
- state: 'opened',
- title: 'some title',
- updated_by_id: 1,
- created_at: '2017-06-27T19:54:42.437Z',
- updated_at: '2017-08-18T03:39:49.222Z',
- time_estimate: 0,
- total_time_spent: 0,
- human_time_estimate: null,
- human_total_time_spent: null,
- branch_name: null,
- confidential: false,
- assignees: [],
- due_date: null,
- moved_to_id: null,
- project_id: 7,
- milestone: null,
- labels: [],
- web_url: '/root/some-project/issues/5',
- },
- '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
- },
-};
+// No new code should be added to this file. Instead, modify the
+// file this one re-exports from. For more detail about why, see:
+// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-const mockData = {
- responseMap: RESPONSE_MAP,
- mediator: {
- endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras',
- toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
- moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
- projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
- editable: true,
- currentUser: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- },
- rootPath: '/',
- fullPath: '/gitlab-org/gitlab-shell',
- },
- time: {
- time_estimate: 3600,
- total_time_spent: 0,
- human_time_estimate: '1h',
- human_total_time_spent: null,
- },
- user: {
- avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 1,
- name: 'Administrator',
- username: 'root',
- },
-};
+import mockData from '../../../spec/frontend/sidebar/mock_data';
export default mockData;
diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
index c7a5ac783b3..caa84faa9c1 100644
--- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
+++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
@@ -38,13 +38,29 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
.and_return(double(execute: kubernetes_namespace))
end
- it { is_expected.to be_falsey }
-
- context 'and the service_account_token is blank' do
- let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: nil) }
+ context 'and the knative version role binding is missing' do
+ before do
+ allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new)
+ .and_return(double(execute: nil))
+ end
it { is_expected.to be_truthy }
end
+
+ context 'and the knative version role binding already exists' do
+ before do
+ allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new)
+ .and_return(double(execute: true))
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'and the service_account_token is blank' do
+ let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: nil) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
end
@@ -115,6 +131,24 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
subject
end
end
+
+ context 'knative version role binding is missing' do
+ before do
+ allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
+ .and_return(double(execute: kubernetes_namespace))
+ allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new)
+ .and_return(double(execute: nil))
+ end
+
+ it 'creates the knative version role binding' do
+ expect(Clusters::Kubernetes::CreateOrUpdateNamespaceService)
+ .to receive(:new)
+ .with(cluster: cluster, kubernetes_namespace: kubernetes_namespace)
+ .and_return(service)
+
+ subject
+ end
+ end
end
context 'completion is not required' do
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 6c9467916de..0ab3e513e24 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -17,13 +17,13 @@ describe Gitlab::Git::Commit, :seed_helper do
@committer = {
email: 'mike@smith.com',
name: "Mike Smith",
- time: Time.now
+ time: Time.new(2000, 1, 1, 0, 0, 0, "+08:00")
}
@author = {
email: 'john@smith.com',
name: "John Smith",
- time: Time.now
+ time: Time.new(2000, 1, 1, 0, 0, 0, "-08:00")
}
@parents = [rugged_repo.head.target]
@@ -48,7 +48,7 @@ describe Gitlab::Git::Commit, :seed_helper do
it { expect(@commit.id).to eq(@raw_commit.oid) }
it { expect(@commit.sha).to eq(@raw_commit.oid) }
it { expect(@commit.safe_message).to eq(@raw_commit.message) }
- it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) }
+ it { expect(@commit.created_at).to eq(@raw_commit.committer[:time]) }
it { expect(@commit.date).to eq(@raw_commit.committer[:time]) }
it { expect(@commit.author_email).to eq(@author[:email]) }
it { expect(@commit.author_name).to eq(@author[:name]) }
@@ -79,13 +79,27 @@ describe Gitlab::Git::Commit, :seed_helper do
it { expect(commit.id).to eq(id) }
it { expect(commit.sha).to eq(id) }
it { expect(commit.safe_message).to eq(body) }
- it { expect(commit.created_at).to eq(Time.at(committer.date.seconds)) }
+ it { expect(commit.created_at).to eq(Time.at(committer.date.seconds).utc) }
it { expect(commit.author_email).to eq(author.email) }
it { expect(commit.author_name).to eq(author.name) }
it { expect(commit.committer_name).to eq(committer.name) }
it { expect(commit.committer_email).to eq(committer.email) }
it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) }
+ context 'non-UTC dates' do
+ let(:seconds) { Time.now.to_i }
+
+ it 'sets timezones correctly' do
+ gitaly_commit.author.date.seconds = seconds
+ gitaly_commit.author.timezone = '-0800'
+ gitaly_commit.committer.date.seconds = seconds
+ gitaly_commit.committer.timezone = '+0800'
+
+ expect(commit.authored_date).to eq(Time.at(seconds, in: '-08:00'))
+ expect(commit.committed_date).to eq(Time.at(seconds, in: '+08:00'))
+ end
+ end
+
context 'body_size != body.size' do
let(:body) { (+"").force_encoding('ASCII-8BIT') }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 916f5536ebd..95435c6dabd 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1197,6 +1197,54 @@ describe Ci::Build do
end
end
+ describe '#expanded_kubernetes_namespace' do
+ let(:build) { create(:ci_build, environment: environment, options: options) }
+
+ subject { build.expanded_kubernetes_namespace }
+
+ context 'environment and namespace are not set' do
+ let(:environment) { nil }
+ let(:options) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'environment is specified' do
+ let(:environment) { 'production' }
+
+ context 'namespace is not set' do
+ let(:options) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'namespace is provided' do
+ let(:options) do
+ {
+ environment: {
+ name: environment,
+ kubernetes: {
+ namespace: namespace
+ }
+ }
+ }
+ end
+
+ context 'with a static value' do
+ let(:namespace) { 'production' }
+
+ it { is_expected.to eq namespace }
+ end
+
+ context 'with a dynamic value' do
+ let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME'}
+
+ it { is_expected.to eq 'deploy-master' }
+ end
+ end
+ end
+ end
+
describe '#starts_environment?' do
subject { build.starts_environment? }
@@ -2987,6 +3035,32 @@ describe Ci::Build do
end
end
+ describe '#deployment_variables' do
+ let(:build) { create(:ci_build, environment: environment) }
+ let(:environment) { 'production' }
+ let(:kubernetes_namespace) { 'namespace' }
+ let(:project_variables) { double }
+
+ subject { build.deployment_variables(environment: environment) }
+
+ before do
+ allow(build).to receive(:expanded_kubernetes_namespace)
+ .and_return(kubernetes_namespace)
+
+ allow(build.project).to receive(:deployment_variables)
+ .with(environment: environment, kubernetes_namespace: kubernetes_namespace)
+ .and_return(project_variables)
+ end
+
+ it { is_expected.to eq(project_variables) }
+
+ context 'environment is nil' do
+ let(:environment) { nil }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '#scoped_variables_hash' do
context 'when overriding CI variables' do
before do
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index d53fc32cfef..4271cf9f1b3 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -290,6 +290,26 @@ describe Clusters::Platforms::Kubernetes do
it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) }
it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) }
+
+ context 'custom namespace is provided' do
+ let(:custom_namespace) { 'custom-namespace' }
+
+ subject do
+ platform.predefined_variables(
+ project: project,
+ environment_name: environment_name,
+ kubernetes_namespace: custom_namespace
+ )
+ end
+
+ before do
+ allow(platform).to receive(:kubeconfig).with(custom_namespace).and_return(kubeconfig)
+ end
+
+ it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
+ it { is_expected.to include(key: 'KUBE_NAMESPACE', value: custom_namespace) }
+ it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) }
+ end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 839c4cadb5e..37c8484d69b 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -359,7 +359,7 @@ eos
it { expect(data).to be_a(Hash) }
it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
- it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') }
+ it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') }
it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") }
it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 45cd2768708..d84a8665dc8 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -106,6 +106,40 @@ describe Milestone do
end
end
+ describe '#merge_requests_enabled?' do
+ context "per project" do
+ it "is true for projects with MRs enabled" do
+ project = create(:project, :merge_requests_enabled)
+ milestone = create(:milestone, project: project)
+
+ expect(milestone.merge_requests_enabled?).to be(true)
+ end
+
+ it "is false for projects with MRs disabled" do
+ project = create(:project, :repository_enabled, :merge_requests_disabled)
+ milestone = create(:milestone, project: project)
+
+ expect(milestone.merge_requests_enabled?).to be(false)
+ end
+
+ it "is false for projects with repository disabled" do
+ project = create(:project, :repository_disabled)
+ milestone = create(:milestone, project: project)
+
+ expect(milestone.merge_requests_enabled?).to be(false)
+ end
+ end
+
+ context "per group" do
+ let(:group) { create(:group) }
+ let(:milestone) { create(:milestone, group: group) }
+
+ it "is always true for groups, for performance reasons" do
+ expect(milestone.merge_requests_enabled?).to be(true)
+ end
+ end
+ end
+
describe "unique milestone title" do
context "per project" do
it "does not accept the same title in a project twice" do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b1f88c4530e..37604a557ea 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2765,8 +2765,9 @@ describe Project do
describe '#deployment_variables' do
let(:project) { create(:project) }
let(:environment) { 'production' }
+ let(:namespace) { 'namespace' }
- subject { project.deployment_variables(environment: environment) }
+ subject { project.deployment_variables(environment: environment, kubernetes_namespace: namespace) }
before do
expect(project).to receive(:deployment_platform).with(environment: environment)
@@ -2785,7 +2786,7 @@ describe Project do
before do
expect(deployment_platform).to receive(:predefined_variables)
- .with(project: project, environment_name: environment)
+ .with(project: project, environment_name: environment, kubernetes_namespace: namespace)
.and_return(platform_variables)
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 675b06b057c..eda2f6d854f 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -131,7 +131,7 @@ describe API::Branches do
end
new_branch_name = 'protected-branch'
- CreateBranchService.new(project, current_user).execute(new_branch_name, 'master')
+ ::Branches::CreateService.new(project, current_user).execute(new_branch_name, 'master')
create(:protected_branch, name: new_branch_name, project: project)
expect do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index ec18156f49f..378441220c7 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -315,11 +315,11 @@ describe API::Files do
expect(range['commit']['message'])
.to eq("Files, encoding and much more\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n")
- expect(range['commit']['authored_date']).to eq('2014-02-27T08:14:56.000Z')
+ expect(range['commit']['authored_date']).to eq('2014-02-27T10:14:56.000+02:00')
expect(range['commit']['author_name']).to eq('Dmitriy Zaporozhets')
expect(range['commit']['author_email']).to eq('dmitriy.zaporozhets@gmail.com')
- expect(range['commit']['committed_date']).to eq('2014-02-27T08:14:56.000Z')
+ expect(range['commit']['committed_date']).to eq('2014-02-27T10:14:56.000+02:00')
expect(range['commit']['committer_name']).to eq('Dmitriy Zaporozhets')
expect(range['commit']['committer_email']).to eq('dmitriy.zaporozhets@gmail.com')
end
diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
new file mode 100644
index 00000000000..4d0bb59b030
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting an issue as confidential' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:input) { { confidential: true } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: issue.iid.to_s
+ }
+ graphql_mutation(:issue_set_confidential, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ confidential
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:issue_set_confidential)
+ end
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns an error if the user is not allowed to update the issue' do
+ error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).to include(a_hash_including('message' => error))
+ end
+
+ it 'updates the issue confidentiality' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['issue']['confidential']).to be_truthy
+ end
+end
diff --git a/spec/requests/user_avatar_spec.rb b/spec/requests/user_avatar_spec.rb
new file mode 100644
index 00000000000..9451674161c
--- /dev/null
+++ b/spec/requests/user_avatar_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Loading a user avatar' do
+ let(:user) { create(:user, :with_avatar) }
+
+ context 'when logged in' do
+ # The exact query count will vary depending on the 2FA settings of the
+ # instance, group, and user. Removing those extra 2FA queries in this case
+ # may not be a good idea, so we just set up the ideal case.
+ before do
+ stub_application_setting(require_two_factor_authentication: true)
+
+ login_as(create(:user, :two_factor))
+ end
+
+ # One each for: current user, avatar user, and upload record
+ it 'only performs three SQL queries' do
+ get user.avatar_url # Skip queries on first application load
+
+ expect(response).to have_gitlab_http_status(200)
+ expect { get user.avatar_url }.not_to exceed_query_limit(3)
+ end
+ end
+
+ context 'when logged out' do
+ # One each for avatar user and upload record
+ it 'only performs two SQL queries' do
+ get user.avatar_url # Skip queries on first application load
+
+ expect(response).to have_gitlab_http_status(200)
+ expect { get user.avatar_url }.not_to exceed_query_limit(2)
+ end
+ end
+end
diff --git a/spec/routing/instance_statistics_routing_spec.rb b/spec/routing/instance_statistics_routing_spec.rb
index b94faabfa1d..48a3ac4695c 100644
--- a/spec/routing/instance_statistics_routing_spec.rb
+++ b/spec/routing/instance_statistics_routing_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Instance Statistics', 'routing' do
include RSpec::Rails::RequestExampleGroup
- it "routes '/-/instance_statistics' to conversational development index" do
- expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/conversational_development_index')
+ it "routes '/-/instance_statistics' to dev ops score" do
+ expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/dev_ops_score')
end
end
diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/branches/create_service_spec.rb
index 9661173c9e7..444491ed6f3 100644
--- a/spec/services/create_branch_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-describe CreateBranchService do
+describe Branches::CreateService do
let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
+ subject(:service) { described_class.new(project, user) }
describe '#execute' do
context 'when repository is empty' do
@@ -30,7 +30,7 @@ describe CreateBranchService do
allow(project.repository).to receive(:add_branch).and_return(false)
end
- it 'retruns an error with the branch name' do
+ it 'returns an error with the branch name' do
result = service.execute('my-feature', 'master')
expect(result[:status]).to eq(:error)
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/branches/delete_merged_service_spec.rb
index dffc2bd93ee..962af8110f7 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/branches/delete_merged_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe DeleteMergedBranchesService do
+describe Branches::DeleteMergedService do
include ProjectForksHelper
subject(:service) { described_class.new(project, project.owner) }
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/branches/delete_service_spec.rb
index b8064c2cbc1..b4848978a6f 100644
--- a/spec/services/delete_branch_service_spec.rb
+++ b/spec/services/branches/delete_service_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-describe DeleteBranchService do
+describe Branches::DeleteService do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
+ subject(:service) { described_class.new(project, user) }
shared_examples 'a deleted branch' do |branch_name|
it 'removes the branch' do
diff --git a/spec/services/branches/validate_new_service_spec.rb b/spec/services/branches/validate_new_service_spec.rb
new file mode 100644
index 00000000000..460f28b5844
--- /dev/null
+++ b/spec/services/branches/validate_new_service_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Branches::ValidateNewService do
+ let(:project) { create(:project, :repository) }
+ subject(:service) { described_class.new(project) }
+
+ describe '#execute' do
+ context 'validation' do
+ it 'returns error with an invalid branch name' do
+ result = service.execute('refs/heads/invalid_branch')
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Branch name is invalid')
+ end
+
+ it 'returns success with a valid branch name' do
+ result = service.execute('valid_branch_name')
+
+ expect(result[:status]).to eq(:success)
+ end
+ end
+
+ context 'branch exist' do
+ it 'returns error when branch exists' do
+ result = service.execute('master')
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Branch already exists')
+ end
+
+ it 'returns success when branch name is available' do
+ result = service.execute('valid_branch_name')
+
+ expect(result[:status]).to eq(:success)
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
index 291e63bbe4a..a9e3e881fd4 100644
--- a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
@@ -22,7 +22,7 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
before do
stub_kubeclient_discover(api_url)
- stub_kubeclient_get_namespace(api_url)
+ stub_kubeclient_get_namespaces(api_url)
stub_kubeclient_get_service_account_error(api_url, 'gitlab')
stub_kubeclient_create_service_account(api_url)
stub_kubeclient_get_secret_error(api_url, 'gitlab-token')
@@ -39,6 +39,8 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace)
stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME, namespace: namespace)
+ stub_kubeclient_put_cluster_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME)
+ stub_kubeclient_put_cluster_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME)
stub_kubeclient_get_secret(
api_url,
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index 4df73fcc2ae..b40861e5aaf 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -141,12 +141,15 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
before do
cluster.platform_kubernetes.rbac!
+ stub_kubeclient_get_namespaces(api_url)
stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace)
stub_kubeclient_create_role_binding(api_url, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace)
stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace)
stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME, namespace: namespace)
+ stub_kubeclient_put_cluster_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME)
+ stub_kubeclient_put_cluster_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME)
end
it_behaves_like 'creates service account and token'
@@ -234,6 +237,30 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
)
)
end
+
+ it 'creates a role and role binding granting the ability to get the version of deployments in knative-serving namespace' do
+ subject
+
+ expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME}").with(
+ body: hash_including(
+ metadata: {
+ name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME
+ },
+ roleRef: {
+ apiGroup: "rbac.authorization.k8s.io",
+ kind: "ClusterRole",
+ name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME
+ },
+ subjects: [
+ {
+ kind: "ServiceAccount",
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
+ )
+ )
+ end
end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 1bdea973685..61c8103353c 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -211,7 +211,8 @@ describe MergeRequests::MergeService do
end
it 'does not delete the source branch' do
- expect(DeleteBranchService).not_to receive(:new)
+ expect(::Branches::DeleteService).not_to receive(:new)
+
service.execute(merge_request)
end
end
@@ -226,7 +227,7 @@ describe MergeRequests::MergeService do
end
it 'does not delete the source branch' do
- expect(DeleteBranchService).not_to receive(:new)
+ expect(::Branches::DeleteService).not_to receive(:new)
service.execute(merge_request)
end
end
@@ -238,7 +239,7 @@ describe MergeRequests::MergeService do
end
it 'removes the source branch using the author user' do
- expect(DeleteBranchService).to receive(:new)
+ expect(::Branches::DeleteService).to receive(:new)
.with(merge_request.source_project, merge_request.author)
.and_call_original
service.execute(merge_request)
@@ -248,7 +249,7 @@ describe MergeRequests::MergeService do
let(:service) { described_class.new(project, user, merge_params.merge('should_remove_source_branch' => false)) }
it 'does not delete the source branch' do
- expect(DeleteBranchService).not_to receive(:new)
+ expect(::Branches::DeleteService).not_to receive(:new)
service.execute(merge_request)
end
end
@@ -260,7 +261,7 @@ describe MergeRequests::MergeService do
end
it 'removes the source branch using the current user' do
- expect(DeleteBranchService).to receive(:new)
+ expect(::Branches::DeleteService).to receive(:new)
.with(merge_request.source_project, user)
.and_call_original
service.execute(merge_request)
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index cccafddc450..77e38f1eb4c 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -61,7 +61,7 @@ describe MergeRequests::MergeToRefService do
end
it 'does not delete the source branch' do
- expect(DeleteBranchService).not_to receive(:new)
+ expect(::Branches::DeleteService).not_to receive(:new)
process_merge_to_ref
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 9d0ad60a624..9e69f179da9 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -113,7 +113,7 @@ describe MergeRequests::RefreshService do
context 'when source branch ref does not exists' do
before do
- DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
+ ::Branches::DeleteService.new(@project, @user).execute(@merge_request.source_branch)
end
it 'closes MRs without source branch ref' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 818794ed956..0d7e17ad52c 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1015,6 +1015,30 @@ describe TodoService do
end
end
+ describe '#mark_todo_as_done' do
+ it 'marks a todo done' do
+ todo1 = create(:todo, :pending, user: john_doe)
+
+ described_class.new.mark_todo_as_done(todo1, john_doe)
+
+ expect(todo1.reload.state).to eq('done')
+ end
+
+ context 'when todo is already in state done' do
+ let(:todo1) { create(:todo, :done, user: john_doe) }
+
+ it 'does not update the todo' do
+ expect { described_class.new.mark_todo_as_done(todo1, john_doe) }.not_to change(todo1.reload, :state)
+ end
+
+ it 'does not update cache count' do
+ expect(john_doe).not_to receive(:update_todos_count_cache)
+
+ described_class.new.mark_todo_as_done(todo1, john_doe)
+ end
+ end
+ end
+
describe '#mark_all_todos_as_done_by_user' do
it 'marks all todos done' do
todo1 = create(:todo, user: john_doe, state: :pending)
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 677aef57661..cac43e94a92 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -194,6 +194,11 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
+ def stub_kubeclient_put_cluster_role_binding(api_url, name)
+ WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
+ .to_return(kube_response({}))
+ end
+
def stub_kubeclient_get_role_binding(api_url, name, namespace: 'default')
WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
.to_return(kube_response({}))
@@ -219,11 +224,21 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
+ def stub_kubeclient_get_namespaces(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/namespaces')
+ .to_return(kube_response(kube_v1_namespace_list_body))
+ end
+
def stub_kubeclient_get_namespace(api_url, namespace: 'default')
WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}")
.to_return(kube_response({}))
end
+ def stub_kubeclient_put_cluster_role(api_url, name)
+ WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterroles/#{name}")
+ .to_return(kube_response({}))
+ end
+
def stub_kubeclient_put_role(api_url, name, namespace: 'default')
WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/roles/#{name}")
.to_return(kube_response({}))
@@ -257,6 +272,20 @@ module KubernetesHelpers
}
end
+ def kube_v1_namespace_list_body
+ {
+ "kind" => "NamespaceList",
+ "apiVersion" => "v1",
+ "items" => [
+ {
+ "metadata" => {
+ "name" => "knative-serving"
+ }
+ }
+ ]
+ }
+ end
+
def kube_v1beta1_discovery_body
{
"kind" => "APIResourceList",
diff --git a/spec/support/helpers/position_tracer_helpers.rb b/spec/support/helpers/position_tracer_helpers.rb
index bbf6e06dd40..7516694d4fe 100644
--- a/spec/support/helpers/position_tracer_helpers.rb
+++ b/spec/support/helpers/position_tracer_helpers.rb
@@ -50,7 +50,7 @@ module PositionTracerHelpers
end
def create_branch(new_name, branch_name)
- CreateBranchService.new(project, current_user).execute(new_name, branch_name)
+ ::Branches::CreateService.new(project, current_user).execute(new_name, branch_name)
end
def create_file(branch_name, file_name, content)
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index c24418b2f90..8962d98218a 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -74,7 +74,7 @@ shared_examples 'handle uploads' do
end
before do
- expect(FileUploader).to receive(:generate_secret).and_return(secret)
+ allow(FileUploader).to receive(:generate_secret).and_return(secret)
UploadService.new(model, jpg, uploader_class).execute
end
@@ -88,6 +88,18 @@ shared_examples 'handle uploads' do
end
end
+ context 'when the upload does not have a MIME type that Rails knows' do
+ let(:po) { fixture_file_upload('spec/fixtures/missing_metadata.po', 'text/plain') }
+
+ it 'falls back to the null type' do
+ UploadService.new(model, po, uploader_class).execute
+
+ get :show, params: params.merge(secret: secret, filename: 'missing_metadata.po')
+
+ expect(response.headers['Content-Type']).to eq('application/octet-stream')
+ end
+ end
+
context "when the model is public" do
before do
model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index c0db4cdde72..da966fd2200 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -8,11 +8,14 @@ RSpec.shared_examples 'a creatable merge request' do
page.within '.dropdown-menu-user' do
click_link user2.name
end
+
expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
+
click_link 'Assign to me'
+
expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
@@ -22,6 +25,7 @@ RSpec.shared_examples 'a creatable merge request' do
page.within '.issue-milestone' do
click_link milestone.title
end
+
expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
@@ -32,6 +36,7 @@ RSpec.shared_examples 'a creatable merge request' do
click_link label.title
click_link label2.title
end
+
page.within '.js-label-select' do
expect(page).to have_content label.title
end
@@ -58,8 +63,9 @@ RSpec.shared_examples 'a creatable merge request' do
it 'updates the branches when selecting a new target project', :js do
target_project_member = target_project.owner
- CreateBranchService.new(target_project, target_project_member)
- .execute('a-brand-new-branch-to-test', 'master')
+ ::Branches::CreateService.new(target_project, target_project_member)
+ .execute('a-brand-new-branch-to-test', 'master')
+
visit project_new_merge_request_path(source_project)
first('.js-target-project').click
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
index a218ca921d9..8c983859e36 100644
--- a/spec/workers/delete_merged_branches_worker_spec.rb
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -8,8 +8,8 @@ describe DeleteMergedBranchesWorker do
let(:project) { create(:project, :repository) }
describe "#perform" do
- it "calls DeleteMergedBranchesService" do
- expect_any_instance_of(DeleteMergedBranchesService).to receive(:execute).and_return(true)
+ it "delegates to Branches::DeleteMergedService" do
+ expect_any_instance_of(::Branches::DeleteMergedService).to receive(:execute).and_return(true)
worker.perform(project.id, project.owner.id)
end