Commit ab7cf450 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 4204cf30
...@@ -31,7 +31,6 @@ rules: ...@@ -31,7 +31,6 @@ rules:
- error - error
- allowElseIf: true - allowElseIf: true
import/no-cycle: warn import/no-cycle: warn
import/no-unresolved: warn
import/no-useless-path-segments: off import/no-useless-path-segments: off
import/order: warn import/order: warn
lines-between-class-members: off lines-between-class-members: off
......
...@@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base ...@@ -20,11 +20,11 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user!, except: [:route_not_found] before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket! 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 :ldap_security_check
before_action :sentry_context before_action :sentry_context
before_action :default_headers 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 :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
before_action :active_user_check, unless: :devise_controller? before_action :active_user_check, unless: :devise_controller?
...@@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base ...@@ -455,8 +455,8 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab')) response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end end
def peek_request? def html_request?
request.path.start_with?('/-/peek') request.format.html?
end end
def json_request? def json_request?
...@@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base ...@@ -466,7 +466,7 @@ class ApplicationController < ActionController::Base
def should_enforce_terms? def should_enforce_terms?
return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
!(peek_request? || devise_controller?) html_request? && !devise_controller?
end end
def set_usage_stats_consent_flag def set_usage_stats_consent_flag
......
...@@ -4,15 +4,18 @@ module ConfirmEmailWarning ...@@ -4,15 +4,18 @@ module ConfirmEmailWarning
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) } before_action :set_confirm_warning, if: :show_confirm_warning?
end end
protected protected
def show_confirm_warning?
html_request? && request.get? && Feature.enabled?(:soft_email_confirmation)
end
def set_confirm_warning def set_confirm_warning
return unless current_user return unless current_user
return if current_user.confirmed? return if current_user.confirmed?
return if peek_request? || json_request? || !request.get?
email = current_user.unconfirmed_email || current_user.email email = current_user.unconfirmed_email || current_user.email
......
...@@ -4,7 +4,7 @@ module SourcegraphGon ...@@ -4,7 +4,7 @@ module SourcegraphGon
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :push_sourcegraph_gon, unless: :json_request? before_action :push_sourcegraph_gon, if: :html_request?
end end
private private
......
# frozen_string_literal: true # frozen_string_literal: true
module UploadsActions module UploadsActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include SendFileUpload include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze 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 def create
uploader = UploadService.new(model, params[:file], uploader_class).execute uploader = UploadService.new(model, params[:file], uploader_class).execute
...@@ -64,6 +69,20 @@ module UploadsActions ...@@ -64,6 +69,20 @@ module UploadsActions
private 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 def uploader_class
raise NotImplementedError raise NotImplementedError
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController class InstanceStatistics::DevOpsScoreController < InstanceStatistics::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
@metric = DevOpsScore::Metric.order(:created_at).last&.present @metric = DevOpsScore::Metric.order(:created_at).last&.present
......
...@@ -46,7 +46,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -46,7 +46,7 @@ class Projects::BranchesController < Projects::ApplicationController
def diverging_commit_counts def diverging_commit_counts
respond_to do |format| respond_to do |format|
format.json do format.json do
service = Branches::DivergingCommitCountsService.new(repository) service = ::Branches::DivergingCommitCountsService.new(repository)
branches = BranchesFinder.new(repository, params.permit(names: [])).execute branches = BranchesFinder.new(repository, params.permit(names: [])).execute
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
...@@ -63,7 +63,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -63,7 +63,7 @@ class Projects::BranchesController < Projects::ApplicationController
redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present? 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) .execute(branch_name, ref)
success = (result[:status] == :success) success = (result[:status] == :success)
...@@ -102,7 +102,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -102,7 +102,7 @@ class Projects::BranchesController < Projects::ApplicationController
def destroy def destroy
@branch_name = Addressable::URI.unescape(params[:id]) @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| respond_to do |format|
format.html do format.html do
...@@ -118,7 +118,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -118,7 +118,7 @@ class Projects::BranchesController < Projects::ApplicationController
end end
def destroy_all_merged 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), 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.') notice: _('Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.')
......
# 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
# 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
...@@ -16,22 +16,21 @@ module Mutations ...@@ -16,22 +16,21 @@ module Mutations
null: false, null: false,
description: 'The requested todo' description: 'The requested todo'
# rubocop: disable CodeReuse/ActiveRecord
def resolve(id:) def resolve(id:)
todo = authorized_find!(id: id) todo = authorized_find!(id: id)
mark_done(Todo.where(id: todo.id)) unless todo.done?
mark_done(todo)
{ {
todo: todo.reset, todo: todo.reset,
errors: errors_on_object(todo) errors: errors_on_object(todo)
} }
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
def mark_done(todo) 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 end
end end
......
...@@ -9,6 +9,7 @@ module Types ...@@ -9,6 +9,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLabels
mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetLocked
......
...@@ -425,6 +425,18 @@ module Ci ...@@ -425,6 +425,18 @@ module Ci
end end
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? def has_environment?
environment.present? environment.present?
end end
......
...@@ -63,7 +63,7 @@ module Clusters ...@@ -63,7 +63,7 @@ module Clusters
default_value_for :authorization_type, :rbac 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| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'KUBE_URL', value: api_url) variables.append(key: 'KUBE_URL', value: api_url)
...@@ -74,15 +74,15 @@ module Clusters ...@@ -74,15 +74,15 @@ module Clusters
end end
if !cluster.managed? || cluster.management_project == project 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 variables
.append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true)
.append(key: 'KUBE_NAMESPACE', value: namespace) .append(key: 'KUBE_NAMESPACE', value: namespace)
.append(key: 'KUBECONFIG', value: kubeconfig(namespace), public: false, file: true) .append(key: 'KUBECONFIG', value: kubeconfig(namespace), public: false, file: true)
elsif kubernetes_namespace = find_persisted_namespace(project, environment_name: environment_name) elsif persisted_namespace = find_persisted_namespace(project, environment_name: environment_name)
variables.concat(kubernetes_namespace.predefined_variables) variables.concat(persisted_namespace.predefined_variables)
end end
variables.concat(cluster.predefined_variables) variables.concat(cluster.predefined_variables)
...@@ -107,6 +107,13 @@ module Clusters ...@@ -107,6 +107,13 @@ module Clusters
private 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:) def find_persisted_namespace(project, environment_name:)
Clusters::KubernetesNamespaceFinder.new( Clusters::KubernetesNamespaceFinder.new(
cluster, cluster,
......
...@@ -15,7 +15,7 @@ module Ci ...@@ -15,7 +15,7 @@ module Ci
variables.concat(project.predefined_variables) variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables) variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runnable? && runner 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(yaml_variables)
variables.concat(user_variables) variables.concat(user_variables)
variables.concat(secret_group_variables) variables.concat(secret_group_variables)
...@@ -72,6 +72,15 @@ module Ci ...@@ -72,6 +72,15 @@ module Ci
end end
end end
def deployment_variables(environment:)
return [] unless environment
project.deployment_variables(
environment: environment,
kubernetes_namespace: expanded_kubernetes_namespace
)
end
def secret_group_variables def secret_group_variables
return [] unless project.group return [] unless project.group
......
...@@ -22,4 +22,8 @@ class DashboardGroupMilestone < GlobalMilestone ...@@ -22,4 +22,8 @@ class DashboardGroupMilestone < GlobalMilestone
def dashboard_milestone? def dashboard_milestone?
true true
end end
def merge_requests_enabled?
true
end
end end
...@@ -12,4 +12,8 @@ class DashboardMilestone < GlobalMilestone ...@@ -12,4 +12,8 @@ class DashboardMilestone < GlobalMilestone
def project_milestone? def project_milestone?
true true
end end
def merge_requests_enabled?
project.merge_requests_enabled?
end
end end
...@@ -41,4 +41,8 @@ class GroupMilestone < GlobalMilestone ...@@ -41,4 +41,8 @@ class GroupMilestone < GlobalMilestone
def legacy_group_milestone? def legacy_group_milestone?
true true
end end
def merge_requests_enabled?
true
end
end end
...@@ -274,6 +274,16 @@ class Milestone < ApplicationRecord ...@@ -274,6 +274,16 @@ class Milestone < ApplicationRecord
project_id.present? project_id.present?
end 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 private
# Milestone titles must be unique across project milestones and group milestones # Milestone titles must be unique across project milestones and group milestones
......
...@@ -1986,12 +1986,16 @@ class Project < ApplicationRecord ...@@ -1986,12 +1986,16 @@ class Project < ApplicationRecord
end end
end end
def deployment_variables(environment:) def deployment_variables(environment:, kubernetes_namespace: nil)
platform = deployment_platform(environment: environment) platform = deployment_platform(environment: environment)
return [] unless platform.present? 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 end
def auto_devops_variables def auto_devops_variables
......
# 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
# 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
# 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
# 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
...@@ -49,8 +49,14 @@ module Clusters ...@@ -49,8 +49,14 @@ module Clusters
create_or_update_knative_serving_role create_or_update_knative_serving_role
create_or_update_knative_serving_role_binding create_or_update_knative_serving_role_binding
create_or_update_crossplane_database_role create_or_update_crossplane_database_role
create_or_update_crossplane_database_role_binding 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 end
private private
...@@ -64,6 +70,12 @@ module Clusters ...@@ -64,6 +70,12 @@ module Clusters
).ensure_exists! ).ensure_exists!
end 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 def create_role_or_cluster_role_binding
if namespace_creator if namespace_creator
kubeclient.create_or_update_role_binding(role_binding_resource) kubeclient.create_or_update_role_binding(role_binding_resource)
...@@ -88,6 +100,14 @@ module Clusters ...@@ -88,6 +100,14 @@ module Clusters
kubeclient.update_role_binding(crossplane_database_role_binding_resource) kubeclient.update_role_binding(crossplane_database_role_binding_resource)
end 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 def service_account_resource
Gitlab::Kubernetes::ServiceAccount.new( Gitlab::Kubernetes::ServiceAccount.new(
service_account_name, service_account_name,
...@@ -166,6 +186,27 @@ module Clusters ...@@ -166,6 +186,27 @@ module Clusters
service_account_name: service_account_name service_account_name: service_account_name
).generate ).generate
end 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 end
end end
...@@ -12,5 +12,8 @@ module Clusters ...@@ -12,5 +12,8 @@ module Clusters
GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding'
GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role' GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role'
GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding' 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
end end
...@@ -32,7 +32,7 @@ module Commits ...@@ -32,7 +32,7 @@ module Commits
end end
def prepare_branch! def prepare_branch!
branch_result = CreateBranchService.new(project, current_user) branch_result = ::Branches::CreateService.new(project, current_user)
.execute(@branch_name, @start_branch) .execute(@branch_name, @start_branch)
if branch_result[:status] != :success if branch_result[:status] != :success
......
...@@ -101,7 +101,7 @@ module Commits ...@@ -101,7 +101,7 @@ module Commits
end end
def validate_new_branch_name! 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 if result[:status] == :error
raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}") raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}")
......
# 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
# 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
# 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
...@@ -19,7 +19,7 @@ module MergeRequests ...@@ -19,7 +19,7 @@ module MergeRequests
return error('Not allowed to create merge request') unless can_create_merge_request? return error('Not allowed to create merge request') unless can_create_merge_request?
return error('Invalid issue iid') unless @issue_iid.present? && issue.present? 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 return result if result[:status] == :error
new_merge_request = create(merge_request) new_merge_request = create(merge_request)
......
...@@ -99,7 +99,7 @@ module MergeRequests ...@@ -99,7 +99,7 @@ module MergeRequests
log_info("Post merge finished on JID #{merge_jid} with state #{state}") log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch? 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) .execute(merge_request.source_branch)
end end
end end
......
...@@ -179,6 +179,14 @@ class TodoService ...@@ -179,6 +179,14 @@ class TodoService
mark_todos_as_done(todos, current_user) mark_todos_as_done(todos, current_user)
end 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 # When user marks some todos as pending
def mark_todos_as_pending(todos, current_user) def mark_todos_as_pending(todos, current_user)
update_todos_state(todos, current_user, :pending) update_todos_state(todos, current_user, :pending)
......
# 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
.prepend-top-default .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 .bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', %button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => _('Dismiss ConvDev introduction') } 'aria-label' => _('Dismiss ConvDev introduction') }
...@@ -9,5 +9,5 @@ ...@@ -9,5 +9,5 @@
= _('Introducing Your Conversational Development Index') = _('Introducing Your Conversational Development Index')
%p %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.') = _('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 .svg-container.convdev
= custom_icon('convdev_overview') = custom_icon('dev_ops_score_overview')
.container.devops-empty .container.devops-empty
.col-sm-12.justify-content-center.text-center .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') %h4= _('Usage ping is not enabled')
- if !current_user.admin? - if !current_user.admin?
%p %p
......
.container.devops-empty .container.devops-empty
.col-sm-12.justify-content-center.text-center .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...') %h4= _('Data is still calculating...')
%p %p
= _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.') = _('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'
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled - usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.container .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' = render 'callout'
.prepend-top-default .prepend-top-default
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= _('index') = _('index')
%br %br
= _('score') = _('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 .devops-cards.board-card-container
- @metric.cards.each do |card| - @metric.cards.each do |card|
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
%li.dropdown %li.dropdown
= render_if_exists 'dashboard/nav_link_list' = render_if_exists 'dashboard/nav_link_list'
- if can?(current_user, :read_instance_statistics) - 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 = link_to instance_statistics_root_path do
= _('Instance Statistics') = _('Instance Statistics')
- if current_user.admin? - if current_user.admin?
......
...@@ -6,15 +6,15 @@ ...@@ -6,15 +6,15 @@
= sprite_icon('chart', size: 24) = sprite_icon('chart', size: 24)
.sidebar-context-title= _('Instance Statistics') .sidebar-context-title= _('Instance Statistics')
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(controller: :conversational_development_index) do = nav_link(controller: :dev_ops_score) do
= link_to instance_statistics_conversational_development_index_index_path do = link_to instance_statistics_dev_ops_score_index_path do
.nav-icon-container .nav-icon-container
= sprite_icon('comment') = sprite_icon('comment')
%span.nav-item-name %span.nav-item-name
= _('ConvDev Index') = _('ConvDev Index')
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :conversational_development_index, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: :dev_ops_score, html_options: { class: "fly-out-top-item" } ) do
= link_to instance_statistics_conversational_development_index_index_path do = link_to instance_statistics_dev_ops_score_index_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('ConvDev Index') = _('ConvDev Index')
......
...@@ -43,8 +43,9 @@ ...@@ -43,8 +43,9 @@
.col-sm-4.milestone-progress .col-sm-4.milestone-progress
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot; - if milestone.merge_requests_enabled?
= link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path &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 .float-lg-right.light #{milestone.percent_complete(current_user)}% complete
.col-sm-2 .col-sm-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
......
...@@ -105,38 +105,39 @@ ...@@ -105,38 +105,39 @@
= render_if_exists 'shared/milestones/weight', milestone: milestone = render_if_exists 'shared/milestones/weight', milestone: milestone
.block.merge-requests - if milestone.merge_requests_enabled?
.sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } .block.merge-requests
%strong .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= custom_icon('mr_bold') %strong
%span= milestone.merge_requests.count = custom_icon('mr_bold')
.title.hide-collapsed %span= milestone.merge_requests.count
Merge requests .title.hide-collapsed
%span.badge.badge-pill= milestone.merge_requests.count Merge requests
.value.hide-collapsed.bold %span.badge.badge-pill= milestone.merge_requests.count
- if !project || can?(current_user, :read_merge_request, project) .value.hide-collapsed.bold
%span.milestone-stat - if !project || can?(current_user, :read_merge_request, project)
= link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do %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: Open:
= milestone.merge_requests.opened.count = milestone.merge_requests.opened.count
%span.milestone-stat %span.milestone-stat
= link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do
Closed: Closed:
= milestone.merge_requests.closed.count = milestone.merge_requests.closed.count
%span.milestone-stat %span.milestone-stat
= link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do
Merged: Merged:
= milestone.merge_requests.merged.count = 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 - if project
- recent_releases, total_count, more_count = recent_releases_with_counts(milestone) - recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
......
...@@ -6,10 +6,11 @@ ...@@ -6,10 +6,11 @@
= link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do
= _('Issues') = _('Issues')
%span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
%li.nav-item - if milestone.merge_requests_enabled?
= link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do %li.nav-item
= _('Merge Requests') = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do
%span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size = _('Merge Requests')
%span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
%li.nav-item %li.nav-item
= link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do = link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do
= _('Participants') = _('Participants')
...@@ -26,9 +27,10 @@ ...@@ -26,9 +27,10 @@
.tab-content.milestone-content .tab-content.milestone-content
.tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } } .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 = 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 - if milestone.merge_requests_enabled?
-# loaded async .tab-pane#tab-merge-requests
= render "shared/milestones/tab_loading" -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants .tab-pane#tab-participants
-# loaded async -# loaded async
= render "shared/milestones/tab_loading" = render "shared/milestones/tab_loading"
......
...@@ -15,7 +15,7 @@ class DeleteMergedBranchesWorker ...@@ -15,7 +15,7 @@ class DeleteMergedBranchesWorker
user = User.find(user_id) user = User.find(user_id)
begin begin
DeleteMergedBranchesService.new(project, user).execute ::Branches::DeleteMergedService.new(project, user).execute
rescue Gitlab::Access::AccessDeniedError rescue Gitlab::Access::AccessDeniedError
return return
end end
......
---
title: Use CI configured namespace for deployments to unmanaged clusters
merge_request: 20686
author:
type: added
---
title: Add GraphQL mutation for setting an issue as confidential
merge_request: 20785
author:
type: added
---
title: Fix Container repositories can not be replicated when s3 is used
merge_request: 21068
author:
type: fixed
---
title: Respect the timezone reported from Gitaly
merge_request: 21066
author:
type: fixed
---
title: Add rbac access to knative-serving namespace deployments to get knative version information
merge_request: 20244
author:
type: changed
---
title: Hide Merge Request information on milestones when MRs are disabled for project
merge_request: 20985
author: Wolfgang Faust
type: changed
# frozen_string_literal: true # frozen_string_literal: true
namespace :instance_statistics do 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 :cohorts, only: :index
resources :conversational_development_index, only: :index resources :dev_ops_score, only: :index
end end
...@@ -102,6 +102,7 @@ const alias = { ...@@ -102,6 +102,7 @@ const alias = {
if (IS_EE) { if (IS_EE) {
Object.assign(alias, { Object.assign(alias, {
ee: path.join(ROOT_PATH, 'ee/app/assets/javascripts'), 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_empty_states: path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'),
ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'), ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'),
ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'), ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'),
...@@ -283,16 +284,13 @@ module.exports = { ...@@ -283,16 +284,13 @@ module.exports = {
jQuery: 'jquery', jQuery: 'jquery',
}), }),
new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, function(resource) { !IS_EE &&
if (Object.keys(module.exports.resolve.alias).indexOf('ee') >= 0) { new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, resource => {
resource.request = resource.request.replace(/^ee_component/, 'ee');
} else {
resource.request = path.join( resource.request = path.join(
ROOT_PATH, ROOT_PATH,
'app/assets/javascripts/vue_shared/components/empty_component.js', 'app/assets/javascripts/vue_shared/components/empty_component.js',
); );
} }),
}),
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{ {
......
...@@ -2518,6 +2518,51 @@ type IssuePermissions { ...@@ -2518,6 +2518,51 @@ type IssuePermissions {
updateIssue: Boolean! updateIssue: Boolean!
} }
"""
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 Autogenerated input type of IssueSetDueDate
""" """
...@@ -3556,6 +3601,7 @@ type Mutation { ...@@ -3556,6 +3601,7 @@ type Mutation {
destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
......
...@@ -14112,6 +14112,33 @@ ...@@ -14112,6 +14112,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "issueSetDueDate",
"description": null, "description": null,
...@@ -14985,6 +15012,136 @@ ...@@ -14985,6 +15012,136 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"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", "kind": "OBJECT",
"name": "IssueSetDueDatePayload", "name": "IssueSetDueDatePayload",
......
...@@ -375,6 +375,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -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 | | `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 | | `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 ### IssueSetDueDatePayload
| Name | Type | Description | | Name | Type | Description |
......
...@@ -55,7 +55,7 @@ The author then adds a comment to this piece of code and adds a link to the issu ...@@ -55,7 +55,7 @@ The author then adds a comment to this piece of code and adds a link to the issu
end 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: - [ ] 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:
``` ```
......
...@@ -26,7 +26,7 @@ If you need to update an existing Karma test file (found in `spec/javascripts`), ...@@ -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 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. 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. 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 As always, please use discretion. Jest solves a lot of issues we experienced in Karma and
......
...@@ -67,7 +67,7 @@ Omnibus installations should add this entry to `gitlab.rb`: ...@@ -67,7 +67,7 @@ Omnibus installations should add this entry to `gitlab.rb`:
gitlab_rails['license_file'] = "/path/to/license/file" 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 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. admin area in the web ui to renew or upgrade licenses.
......
...@@ -116,7 +116,8 @@ You must do the following: ...@@ -116,7 +116,8 @@ You must do the following:
1. Ensure GitLab can manage Knative: 1. Ensure GitLab can manage Knative:
- For a non-GitLab managed cluster, ensure that the service account for the token - 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), - 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. 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: ...@@ -153,6 +154,19 @@ You must do the following:
- delete - delete
- patch - patch
- watch - 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: Then run the following command:
......
...@@ -137,7 +137,7 @@ module API ...@@ -137,7 +137,7 @@ module API
post ':id/repository/branches' do post ':id/repository/branches' do
authorize_push_project authorize_push_project
result = CreateBranchService.new(user_project, current_user) result = ::Branches::CreateService.new(user_project, current_user)
.execute(params[:branch], params[:ref]) .execute(params[:branch], params[:ref])
if result[:status] == :success if result[:status] == :success
...@@ -162,7 +162,7 @@ module API ...@@ -162,7 +162,7 @@ module API
commit = user_project.repository.commit(branch.dereferenced_target) commit = user_project.repository.commit(branch.dereferenced_target)
destroy_conditionally!(commit, last_updated: commit.authored_date) do 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]) .execute(params[:branch])
if result.error? if result.error?
...@@ -173,7 +173,7 @@ module API ...@@ -173,7 +173,7 @@ module API
desc 'Delete all merged branches' desc 'Delete all merged branches'
delete ':id/repository/merged_branches' do delete ':id/repository/merged_branches' do
DeleteMergedBranchesService.new(user_project, current_user).async_execute ::Branches::DeleteMergedService.new(user_project, current_user).async_execute
accepted! accepted!
end end
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
def unmet? def unmet?
deployment_cluster.present? && deployment_cluster.present? &&
deployment_cluster.managed? && deployment_cluster.managed? &&
missing_namespace? (missing_namespace? || missing_knative_version_role_binding?)
end end
def complete! def complete!
...@@ -23,6 +23,10 @@ module Gitlab ...@@ -23,6 +23,10 @@ module Gitlab
kubernetes_namespace.nil? || kubernetes_namespace.service_account_token.blank? kubernetes_namespace.nil? || kubernetes_namespace.service_account_token.blank?
end end
def missing_knative_version_role_binding?
knative_version_role_binding.nil?
end
def deployment_cluster def deployment_cluster
build.deployment&.cluster build.deployment&.cluster
end end
...@@ -31,6 +35,14 @@ module Gitlab ...@@ -31,6 +35,14 @@ module Gitlab
build.deployment.environment build.deployment.environment
end end
def knative_version_role_binding
strong_memoize(:knative_version_role_binding) do
Clusters::KnativeVersionRoleBindingFinder.new(
deployment_cluster
).execute
end
end
def kubernetes_namespace def kubernetes_namespace
strong_memoize(:kubernetes_namespace) do strong_memoize(:kubernetes_namespace) do
Clusters::KubernetesNamespaceFinder.new( Clusters::KubernetesNamespaceFinder.new(
......
...@@ -370,15 +370,26 @@ module Gitlab ...@@ -370,15 +370,26 @@ module Gitlab
# subject from the message to make it clearer when there's one # subject from the message to make it clearer when there's one
# available but not the other. # available but not the other.
@message = message_from_gitaly_body @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_name = commit.author.name.dup
@author_email = commit.author.email.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_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup @committer_email = commit.committer.email.dup
@parent_ids = Array(commit.parent_ids) @parent_ids = Array(commit.parent_ids)
end 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 def serialize_keys
SERIALIZE_KEYS SERIALIZE_KEYS
end end
......
# 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
...@@ -56,6 +56,7 @@ module Gitlab ...@@ -56,6 +56,7 @@ module Gitlab
# group client # group client
delegate :create_cluster_role_binding, delegate :create_cluster_role_binding,
:get_cluster_role_binding, :get_cluster_role_binding,
:get_cluster_role_bindings,
:update_cluster_role_binding, :update_cluster_role_binding,
to: :rbac_client to: :rbac_client
...@@ -66,6 +67,13 @@ module Gitlab ...@@ -66,6 +67,13 @@ module Gitlab
:update_role, :update_role,
to: :rbac_client to: :rbac_client
# 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 # RBAC methods delegates to the apis/rbac.authorization.k8s.io api
# group client # group client
delegate :create_role_binding, delegate :create_role_binding,
......
...@@ -7,10 +7,10 @@ module RuboCop ...@@ -7,10 +7,10 @@ module RuboCop
# #
# @example # @example
# # bad # # bad
# root to: redirect('/-/instance/statistics/conversational_development_index') # root to: redirect('/-/instance/statistics/dev_ops_score')
# #
# # good # # good
# root to: redirect('-/instance/statistics/conversational_development_index') # root to: redirect('-/instance/statistics/dev_ops_score')
# #
class AvoidRouteRedirectLeadingSlash < RuboCop::Cop::Cop class AvoidRouteRedirectLeadingSlash < RuboCop::Cop::Cop
......
...@@ -90,14 +90,6 @@ describe ApplicationController do ...@@ -90,14 +90,6 @@ describe ApplicationController do
let(:format) { :html } let(:format) { :html }
it_behaves_like 'setting gon variables' 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 end
context 'with json format' do context 'with json format' do
...@@ -105,6 +97,12 @@ describe ApplicationController do ...@@ -105,6 +97,12 @@ describe ApplicationController do
it_behaves_like 'not setting gon variables' it_behaves_like 'not setting gon variables'
end end
context 'with atom format' do
let(:format) { :atom }
it_behaves_like 'not setting gon variables'
end
end end
describe 'session expiration' do describe 'session expiration' do
......
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
require 'spec_helper' require 'spec_helper'
describe InstanceStatistics::ConversationalDevelopmentIndexController do describe InstanceStatistics::DevOpsScoreController do
it_behaves_like 'instance statistics availability' it_behaves_like 'instance statistics availability'
end end
...@@ -178,7 +178,7 @@ describe Projects::BranchesController do ...@@ -178,7 +178,7 @@ describe Projects::BranchesController do
it 'redirects to newly created branch' do it 'redirects to newly created branch' do
result = { status: :success, branch: double(name: branch) } 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) expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
post :create, post :create,
...@@ -200,7 +200,7 @@ describe Projects::BranchesController do ...@@ -200,7 +200,7 @@ describe Projects::BranchesController do
it 'redirects to autodeploy setup page' do it 'redirects to autodeploy setup page' do
result = { status: :success, branch: double(name: branch) } 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) expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
post :create, post :create,
...@@ -221,7 +221,7 @@ describe Projects::BranchesController do ...@@ -221,7 +221,7 @@ describe Projects::BranchesController do
create(:cluster, :provided_by_gcp, projects: [project]) 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) expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
post :create, post :create,
...@@ -459,7 +459,7 @@ describe Projects::BranchesController do ...@@ -459,7 +459,7 @@ describe Projects::BranchesController do
end end
it 'starts worker to delete merged branches' do 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 destroy_all_merged
end end
......
...@@ -228,10 +228,10 @@ describe UploadsController do ...@@ -228,10 +228,10 @@ describe UploadsController do
user.block user.block
end 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" } 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
end end
...@@ -320,10 +320,10 @@ describe UploadsController do ...@@ -320,10 +320,10 @@ describe UploadsController do
end end
context "when not signed in" do 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" } 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
end end
...@@ -343,10 +343,10 @@ describe UploadsController do ...@@ -343,10 +343,10 @@ describe UploadsController do
project.add_maintainer(user) project.add_maintainer(user)
end 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" } 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
end end
...@@ -439,10 +439,10 @@ describe UploadsController do ...@@ -439,10 +439,10 @@ describe UploadsController do
user.block user.block
end 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" } 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
end end
...@@ -526,10 +526,10 @@ describe UploadsController do ...@@ -526,10 +526,10 @@ describe UploadsController do
end end
context "when not signed in" do 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" } 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
end end
...@@ -549,10 +549,10 @@ describe UploadsController do ...@@ -549,10 +549,10 @@ describe UploadsController do
project.add_maintainer(user) project.add_maintainer(user)
end 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" } 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
end end
......
...@@ -30,6 +30,7 @@ describe 'Dashboard > Milestones' do ...@@ -30,6 +30,7 @@ describe 'Dashboard > Milestones' do
expect(current_path).to eq dashboard_milestones_path expect(current_path).to eq dashboard_milestones_path
expect(page).to have_content(milestone.title) expect(page).to have_content(milestone.title)
expect(page).to have_content(group.name) expect(page).to have_content(group.name)
expect(first('.milestone')).to have_content('Merge Requests')
end end
describe 'new milestones dropdown', :js do describe 'new milestones dropdown', :js do
...@@ -46,4 +47,23 @@ describe 'Dashboard > Milestones' do ...@@ -46,4 +47,23 @@ describe 'Dashboard > Milestones' do
end end
end 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 end
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
require 'spec_helper' require 'spec_helper'
describe 'Conversational Development Index' do describe 'Dev Ops Score' do
before do before do
sign_in(create(:admin)) sign_in(create(:admin))
end end
it 'has dismissable intro callout', :js do 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' expect(page).to have_content 'Introducing Your Conversational Development Index'
...@@ -23,13 +23,13 @@ describe 'Conversational Development Index' do ...@@ -23,13 +23,13 @@ describe 'Conversational Development Index' do
end end
it 'shows empty state' do 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') expect(page).to have_content('Usage ping is not enabled')
end end
it 'hides the intro callout' do 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' expect(page).not_to have_content 'Introducing Your Conversational Development Index'
end end
...@@ -39,7 +39,7 @@ describe 'Conversational Development Index' do ...@@ -39,7 +39,7 @@ describe 'Conversational Development Index' do
it 'shows empty state' do it 'shows empty state' do
stub_application_setting(usage_ping_enabled: true) 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') expect(page).to have_content('Data is still calculating')
end end
...@@ -50,7 +50,7 @@ describe 'Conversational Development Index' do ...@@ -50,7 +50,7 @@ describe 'Conversational Development Index' do
stub_application_setting(usage_ping_enabled: true) stub_application_setting(usage_ping_enabled: true)
create(:dev_ops_score_metric) 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( expect(page).to have_content(
'Issues created per active user 1.2 You 9.3 Lead 13.3%' 'Issues created per active user 1.2 You 9.3 Lead 13.3%'
......
...@@ -9,7 +9,7 @@ describe 'Merge request > User sees deleted target branch', :js do ...@@ -9,7 +9,7 @@ describe 'Merge request > User sees deleted target branch', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
DeleteBranchService.new(project, user).execute('feature') ::Branches::DeleteService.new(project, user).execute('feature')
sign_in(user) sign_in(user)
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
end end
......
...@@ -178,10 +178,11 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -178,10 +178,11 @@ describe 'Merge request > User selects branches for new MR', :js do
end end
context 'with special characters in branch names' do context 'with special characters in branch names' do
let(:create_branch_service) { ::Branches::CreateService.new(project, user) }
it 'escapes quotes in branch names' do it 'escapes quotes in branch names' do
special_branch_name = '"with-quotes"' special_branch_name = '"with-quotes"'
CreateBranchService.new(project, user) create_branch_service.execute(special_branch_name, 'add-pdf-file')
.execute(special_branch_name, 'add-pdf-file')
visit project_new_merge_request_path(project) visit project_new_merge_request_path(project)
select_source_branch(special_branch_name) select_source_branch(special_branch_name)
...@@ -192,8 +193,7 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -192,8 +193,7 @@ describe 'Merge request > User selects branches for new MR', :js do
it 'does not escape unicode in branch names' do it 'does not escape unicode in branch names' do
special_branch_name = 'ʕ•ᴥ•ʔ' special_branch_name = 'ʕ•ᴥ•ʔ'
CreateBranchService.new(project, user) create_branch_service.execute(special_branch_name, 'add-pdf-file')
.execute(special_branch_name, 'add-pdf-file')
visit project_new_merge_request_path(project) visit project_new_merge_request_path(project)
select_source_branch(special_branch_name) select_source_branch(special_branch_name)
......
...@@ -18,6 +18,7 @@ describe "User views milestones" do ...@@ -18,6 +18,7 @@ describe "User views milestones" do
expect(page).to have_content(milestone.title) expect(page).to have_content(milestone.title)
.and have_content(milestone.expires_at) .and have_content(milestone.expires_at)
.and have_content("Issues") .and have_content("Issues")
.and have_content("Merge Requests")
end end
context "with issues" do context "with issues" do
...@@ -32,6 +33,7 @@ describe "User views milestones" do ...@@ -32,6 +33,7 @@ describe "User views milestones" do
.and have_selector("#tab-issues li.issuable-row", count: 2) .and have_selector("#tab-issues li.issuable-row", count: 2)
.and have_content(issue.title) .and have_content(issue.title)
.and have_content(closed_issue.title) .and have_content(closed_issue.title)
.and have_selector("#tab-merge-requests")
end end
end end
...@@ -62,3 +64,32 @@ describe "User views milestones" do ...@@ -62,3 +64,32 @@ describe "User views milestones" do
end end
end 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
...@@ -31,6 +31,7 @@ class CustomEnvironment extends JSDOMEnvironment { ...@@ -31,6 +31,7 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.gon = { this.global.gon = {
ee: IS_EE, ee: IS_EE,
}; };
this.global.IS_EE = IS_EE;
this.rejectedPromises = []; this.rejectedPromises = [];
......
// 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 mockHost = 'http://test.host';
export const mockProjectDir = '/frontend-fixtures/environments-project'; export const mockProjectDir = '/frontend-fixtures/environments-project';
export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`;
export const anomalyDeploymentData = [ export const anomalyDeploymentData = [
{ {
...@@ -278,6 +283,49 @@ export const mockedQueryResultPayload = { ...@@ -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 = [ export const metricsGroupsAPIResponse = [
{ {
group: 'System metrics (Kubernetes)', group: 'System metrics (Kubernetes)',
...@@ -460,3 +508,130 @@ export const dashboardGitResponse = [ ...@@ -460,3 +508,130 @@ export const dashboardGitResponse = [
path: '.gitlab/dashboards/dashboard_2.yml', 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],
],
},
],
},
],
};
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);
});
});
});
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;
# 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
import { // No new code should be added to this file. Instead, modify the
anomalyMockGraphData as importedAnomalyMockGraphData, // file this one re-exports from. For more detail about why, see:
metricsGroupsAPIResponse as importedMetricsGroupsAPIResponse, // https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
environmentData as importedEnvironmentData,
dashboardGitResponse as importedDashboardGitResponse,
} from '../../frontend/monitoring/mock_data';
export const anomalyMockGraphData = importedAnomalyMockGraphData; export * from '../../frontend/monitoring/mock_data';
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],
],
},
],
},
],
};
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);
});
});
});
const RESPONSE_MAP = { // No new code should be added to this file. Instead, modify the
GET: { // file this one re-exports from. For more detail about why, see:
'/gitlab-org/gitlab-shell/issues/5.json': { // https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
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 = { import mockData from '../../../spec/frontend/sidebar/mock_data';
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; export default mockData;
...@@ -38,13 +38,29 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do ...@@ -38,13 +38,29 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
.and_return(double(execute: kubernetes_namespace)) .and_return(double(execute: kubernetes_namespace))
end end
it { is_expected.to be_falsey } context 'and the knative version role binding is missing' do
before do
context 'and the service_account_token is blank' do allow(Clusters::KnativeVersionRoleBindingFinder).to receive(:new)
let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: nil) } .and_return(double(execute: nil))
end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end 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
end end
...@@ -115,6 +131,24 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do ...@@ -115,6 +131,24 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
subject subject
end end
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 end
context 'completion is not required' do context 'completion is not required' do
......
...@@ -17,13 +17,13 @@ describe Gitlab::Git::Commit, :seed_helper do ...@@ -17,13 +17,13 @@ describe Gitlab::Git::Commit, :seed_helper do
@committer = { @committer = {
email: 'mike@smith.com', email: 'mike@smith.com',
name: "Mike Smith", name: "Mike Smith",
time: Time.now time: Time.new(2000, 1, 1, 0, 0, 0, "+08:00")
} }
@author = { @author = {
email: 'john@smith.com', email: 'john@smith.com',
name: "John Smith", name: "John Smith",
time: Time.now time: Time.new(2000, 1, 1, 0, 0, 0, "-08:00")
} }
@parents = [rugged_repo.head.target] @parents = [rugged_repo.head.target]
...@@ -48,7 +48,7 @@ describe Gitlab::Git::Commit, :seed_helper do ...@@ -48,7 +48,7 @@ describe Gitlab::Git::Commit, :seed_helper do
it { expect(@commit.id).to eq(@raw_commit.oid) } it { expect(@commit.id).to eq(@raw_commit.oid) }
it { expect(@commit.sha).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.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.date).to eq(@raw_commit.committer[:time]) }
it { expect(@commit.author_email).to eq(@author[:email]) } it { expect(@commit.author_email).to eq(@author[:email]) }
it { expect(@commit.author_name).to eq(@author[:name]) } it { expect(@commit.author_name).to eq(@author[:name]) }
...@@ -79,13 +79,27 @@ describe Gitlab::Git::Commit, :seed_helper do ...@@ -79,13 +79,27 @@ describe Gitlab::Git::Commit, :seed_helper do
it { expect(commit.id).to eq(id) } it { expect(commit.id).to eq(id) }
it { expect(commit.sha).to eq(id) } it { expect(commit.sha).to eq(id) }
it { expect(commit.safe_message).to eq(body) } 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_email).to eq(author.email) }
it { expect(commit.author_name).to eq(author.name) } it { expect(commit.author_name).to eq(author.name) }
it { expect(commit.committer_name).to eq(committer.name) } it { expect(commit.committer_name).to eq(committer.name) }
it { expect(commit.committer_email).to eq(committer.email) } it { expect(commit.committer_email).to eq(committer.email) }
it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } 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 context 'body_size != body.size' do
let(:body) { (+"").force_encoding('ASCII-8BIT') } let(:body) { (+"").force_encoding('ASCII-8BIT') }
......
...@@ -1197,6 +1197,54 @@ describe Ci::Build do ...@@ -1197,6 +1197,54 @@ describe Ci::Build do
end end
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 describe '#starts_environment?' do
subject { build.starts_environment? } subject { build.starts_environment? }
...@@ -2987,6 +3035,32 @@ describe Ci::Build do ...@@ -2987,6 +3035,32 @@ describe Ci::Build do
end end
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 describe '#scoped_variables_hash' do
context 'when overriding CI variables' do context 'when overriding CI variables' do
before do before do
......
...@@ -290,6 +290,26 @@ describe Clusters::Platforms::Kubernetes do ...@@ -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_TOKEN', value: platform.token, public: false, masked: true) }
it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) } it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) }
it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) } 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
end end
......
...@@ -359,7 +359,7 @@ eos ...@@ -359,7 +359,7 @@ eos
it { expect(data).to be_a(Hash) } 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[: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[:added]).to contain_exactly("bar/branch-test.txt") }
it { expect(data[:modified]).to eq([]) } it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) } it { expect(data[:removed]).to eq([]) }
......
...@@ -106,6 +106,40 @@ describe Milestone do ...@@ -106,6 +106,40 @@ describe Milestone do
end end
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 describe "unique milestone title" do
context "per project" do context "per project" do
it "does not accept the same title in a project twice" do it "does not accept the same title in a project twice" do
......
...@@ -2765,8 +2765,9 @@ describe Project do ...@@ -2765,8 +2765,9 @@ describe Project do
describe '#deployment_variables' do describe '#deployment_variables' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:environment) { 'production' } let(:environment) { 'production' }
let(:namespace) { 'namespace' }
subject { project.deployment_variables(environment: environment) } subject { project.deployment_variables(environment: environment, kubernetes_namespace: namespace) }
before do before do
expect(project).to receive(:deployment_platform).with(environment: environment) expect(project).to receive(:deployment_platform).with(environment: environment)
...@@ -2785,7 +2786,7 @@ describe Project do ...@@ -2785,7 +2786,7 @@ describe Project do
before do before do
expect(deployment_platform).to receive(:predefined_variables) 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) .and_return(platform_variables)
end end
......
...@@ -131,7 +131,7 @@ describe API::Branches do ...@@ -131,7 +131,7 @@ describe API::Branches do
end end
new_branch_name = 'protected-branch' 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) create(:protected_branch, name: new_branch_name, project: project)
expect do expect do
......
...@@ -315,11 +315,11 @@ describe API::Files do ...@@ -315,11 +315,11 @@ describe API::Files do
expect(range['commit']['message']) expect(range['commit']['message'])
.to eq("Files, encoding and much more\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n") .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_name']).to eq('Dmitriy Zaporozhets')
expect(range['commit']['author_email']).to eq('dmitriy.zaporozhets@gmail.com') 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_name']).to eq('Dmitriy Zaporozhets')
expect(range['commit']['committer_email']).to eq('dmitriy.zaporozhets@gmail.com') expect(range['commit']['committer_email']).to eq('dmitriy.zaporozhets@gmail.com')
end end
......
# 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
# 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
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Instance Statistics', 'routing' do describe 'Instance Statistics', 'routing' do
include RSpec::Rails::RequestExampleGroup include RSpec::Rails::RequestExampleGroup
it "routes '/-/instance_statistics' to conversational development index" do it "routes '/-/instance_statistics' to dev ops score" do
expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/conversational_development_index') expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/dev_ops_score')
end end
end end
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe CreateBranchService do describe Branches::CreateService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { described_class.new(project, user) } subject(:service) { described_class.new(project, user) }
describe '#execute' do describe '#execute' do
context 'when repository is empty' do context 'when repository is empty' do
...@@ -30,7 +30,7 @@ describe CreateBranchService do ...@@ -30,7 +30,7 @@ describe CreateBranchService do
allow(project.repository).to receive(:add_branch).and_return(false) allow(project.repository).to receive(:add_branch).and_return(false)
end 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') result = service.execute('my-feature', 'master')
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment