summaryrefslogtreecommitdiff
path: root/app/services
diff options
context:
space:
mode:
Diffstat (limited to 'app/services')
-rw-r--r--app/services/application_settings/update_service.rb29
-rw-r--r--app/services/boards/issues/list_service.rb6
-rw-r--r--app/services/boards/lists/create_service.rb6
-rw-r--r--app/services/boards/lists/list_service.rb2
-rw-r--r--app/services/boards/lists/update_service.rb16
-rw-r--r--app/services/bulk_push_event_payload_service.rb19
-rw-r--r--app/services/ci/pipeline_trigger_service.rb29
-rw-r--r--app/services/ci/process_pipeline_service.rb7
-rw-r--r--app/services/ci/retry_build_service.rb11
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb8
-rw-r--r--app/services/clusters/gcp/provision_service.rb7
-rw-r--r--app/services/concerns/git/change_params.rb17
-rw-r--r--app/services/deployments/after_create_service.rb60
-rw-r--r--app/services/deployments/create_service.rb39
-rw-r--r--app/services/deployments/update_service.rb16
-rw-r--r--app/services/event_create_service.rb24
-rw-r--r--app/services/git/base_hooks_service.rb28
-rw-r--r--app/services/git/branch_hooks_service.rb16
-rw-r--r--app/services/git/branch_push_service.rb20
-rw-r--r--app/services/git/process_ref_changes_service.rb75
-rw-r--r--app/services/git/tag_hooks_service.rb6
-rw-r--r--app/services/git/tag_push_service.rb4
-rw-r--r--app/services/grafana/proxy_service.rb83
-rw-r--r--app/services/groups/create_service.rb7
-rw-r--r--app/services/groups/transfer_service.rb8
-rw-r--r--app/services/groups/update_service.rb16
-rw-r--r--app/services/import_export_clean_up_service.rb14
-rw-r--r--app/services/issuable/clone/content_rewriter.rb4
-rw-r--r--app/services/issues/close_service.rb4
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/issues/zoom_link_service.rb22
-rw-r--r--app/services/merge_requests/post_merge_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb26
-rw-r--r--app/services/merge_requests/update_service.rb3
-rw-r--r--app/services/note_summary.rb4
-rw-r--r--app/services/notes/quick_actions_service.rb2
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb24
-rw-r--r--app/services/notification_service.rb9
-rw-r--r--app/services/projects/after_import_service.rb9
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb2
-rw-r--r--app/services/projects/container_repository/delete_tags_service.rb66
-rw-r--r--app/services/projects/create_from_template_service.rb9
-rw-r--r--app/services/projects/create_service.rb6
-rw-r--r--app/services/projects/destroy_service.rb4
-rw-r--r--app/services/projects/fork_service.rb1
-rw-r--r--app/services/projects/hashed_storage/base_repository_service.rb10
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb1
-rw-r--r--app/services/projects/hashed_storage/rollback_repository_service.rb1
-rw-r--r--app/services/projects/import_export/export_service.rb38
-rw-r--r--app/services/projects/operations/update_service.rb12
-rw-r--r--app/services/projects/update_pages_service.rb1
-rw-r--r--app/services/search/snippet_service.rb12
-rw-r--r--app/services/spam_service.rb3
-rw-r--r--app/services/system_note_service.rb405
-rw-r--r--app/services/system_notes/base_service.rb30
-rw-r--r--app/services/system_notes/commit_service.rb112
-rw-r--r--app/services/system_notes/issuables_service.rb312
-rw-r--r--app/services/system_notes/zoom_service.rb13
-rw-r--r--app/services/update_deployment_service.rb57
60 files changed, 1197 insertions, 586 deletions
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 6400b182715..df9217bea32 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -20,7 +20,11 @@ module ApplicationSettings
add_to_outbound_local_requests_whitelist(@params.delete(:add_to_outbound_local_requests_whitelist))
if params.key?(:performance_bar_allowed_group_path)
- params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
+ group_id = process_performance_bar_allowed_group_id
+
+ return false if application_setting.errors.any?
+
+ params[:performance_bar_allowed_group_id] = group_id
end
if usage_stats_updated? && !params.delete(:skip_usage_stats_user)
@@ -65,12 +69,27 @@ module ApplicationSettings
@application_setting.reset_memoized_terms
end
- def performance_bar_allowed_group_id
- performance_bar_enabled = !params.key?(:performance_bar_enabled) || params.delete(:performance_bar_enabled)
+ def process_performance_bar_allowed_group_id
group_full_path = params.delete(:performance_bar_allowed_group_path)
- return unless Gitlab::Utils.to_boolean(performance_bar_enabled)
+ enable_param_on = Gitlab::Utils.to_boolean(params.delete(:performance_bar_enabled))
+ performance_bar_enabled = enable_param_on.nil? || enable_param_on # Default to true
+
+ return if group_full_path.blank?
+ return if enable_param_on == false # Explicitly disabling
+
+ unless performance_bar_enabled
+ application_setting.errors.add(:performance_bar_allowed_group_id, 'not allowed when performance bar is disabled')
+ return
+ end
+
+ group = Group.find_by_full_path(group_full_path.chomp('/'))
+
+ unless group
+ application_setting.errors.add(:performance_bar_allowed_group_id, 'not found')
+ return
+ end
- Group.find_by_full_path(group_full_path)&.id if group_full_path.present?
+ group.id
end
def bypass_external_auth?
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 10eb1141f59..37a74cd1b00 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -11,9 +11,11 @@ module Boards
# rubocop: disable CodeReuse/ActiveRecord
def metadata
+ issues = Issue.arel_table
keys = metadata_fields.keys
- columns = metadata_fields.values_at(*keys).join(', ')
- results = Issue.where(id: fetch_issues.select('issues.id')).pluck(columns)
+ # TODO: eliminate need for SQL literal fragment
+ columns = Arel.sql(metadata_fields.values_at(*keys).join(', '))
+ results = Issue.where(id: fetch_issues.select(issues[:id])).pluck(columns)
Hash[keys.zip(results.flatten)]
end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index eb417ac4f5f..6f9a063cb16 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -43,7 +43,11 @@ module Boards
end
def create_list(board, type, target, position)
- board.lists.create(type => target, list_type: type, position: position)
+ board.lists.create(create_list_attributes(type, target, position))
+ end
+
+ def create_list_attributes(type, target, position)
+ { type => target, list_type: type, position: position }
end
end
end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
index 3609d9c6283..82cba1b68c4 100644
--- a/app/services/boards/lists/list_service.rb
+++ b/app/services/boards/lists/list_service.rb
@@ -6,7 +6,7 @@ module Boards
def execute(board)
board.lists.create(list_type: :backlog) unless board.lists.backlog.exists?
- board.lists.preload_associations(current_user)
+ board.lists.preload_associations
end
end
end
diff --git a/app/services/boards/lists/update_service.rb b/app/services/boards/lists/update_service.rb
index ad96e42f756..4a463372c82 100644
--- a/app/services/boards/lists/update_service.rb
+++ b/app/services/boards/lists/update_service.rb
@@ -4,16 +4,22 @@ module Boards
module Lists
class UpdateService < Boards::BaseService
def execute(list)
- update_preferences_result = update_preferences(list) if can_read?(list)
- update_position_result = update_position(list) if can_admin?(list)
-
- if update_preferences_result || update_position_result
+ if execute_by_params(list)
success(list: list)
else
error(list.errors.messages, 422)
end
end
+ private
+
+ def execute_by_params(list)
+ update_preferences_result = update_preferences(list) if can_read?(list)
+ update_position_result = update_position(list) if can_admin?(list)
+
+ update_preferences_result || update_position_result
+ end
+
def update_preferences(list)
return unless preferences?
@@ -50,3 +56,5 @@ module Boards
end
end
end
+
+Boards::Lists::UpdateService.prepend_if_ee('EE::Boards::Lists::UpdateService')
diff --git a/app/services/bulk_push_event_payload_service.rb b/app/services/bulk_push_event_payload_service.rb
new file mode 100644
index 00000000000..54157bc23f9
--- /dev/null
+++ b/app/services/bulk_push_event_payload_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class BulkPushEventPayloadService
+ def initialize(event, push_data)
+ @event = event
+ @push_data = push_data
+ end
+
+ def execute
+ @event.build_push_event_payload(
+ action: @push_data[:action],
+ commit_count: 0,
+ ref_count: @push_data[:ref_count],
+ ref_type: @push_data[:ref_type]
+ )
+
+ @event.push_event_payload.tap(&:save!)
+ end
+end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 0e99f142492..37b9b4c362c 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -38,11 +38,34 @@ module Ci
end
def create_pipeline_from_job(job)
- # overridden in EE
+ # this check is to not leak the presence of the project if user cannot read it
+ return unless can?(job.user, :read_project, project)
+
+ return error("400 Job has to be running", 400) unless job.running?
+
+ pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
+ .execute(:pipeline, ignore_skip_ci: true) do |pipeline|
+ source = job.sourced_pipelines.build(
+ source_pipeline: job.pipeline,
+ source_project: job.project,
+ pipeline: pipeline,
+ project: project)
+
+ pipeline.source_pipeline = source
+ pipeline.variables.build(variables)
+ end
+
+ if pipeline.persisted?
+ success(pipeline: pipeline)
+ else
+ error(pipeline.errors.messages, 400)
+ end
end
def job_from_token
- # overridden in EE
+ strong_memoize(:job) do
+ Ci::Build.find_by_token(params[:token].to_s)
+ end
end
def variables
@@ -52,5 +75,3 @@ module Ci
end
end
end
-
-Ci::PipelineTriggerService.prepend_if_ee('EE::Ci::PipelineTriggerService')
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 3b145a65d79..039670f58c8 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -2,6 +2,8 @@
module Ci
class ProcessPipelineService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :pipeline
def execute(pipeline, trigger_build_ids = nil)
@@ -33,9 +35,9 @@ module Ci
return unless HasStatus::COMPLETED_STATUSES.include?(current_status)
- created_processables_in_stage_without_needs(index).select do |build|
+ created_processables_in_stage_without_needs(index).find_each.select do |build|
process_build(build, current_status)
- end
+ end.any?
end
def process_builds_with_needs(trigger_build_ids)
@@ -92,6 +94,7 @@ module Ci
def created_processables_in_stage_without_needs(index)
created_processables_without_needs
+ .with_preloads
.for_stage(index)
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 338495ba030..7a5e33c61ba 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -39,9 +39,18 @@ module Ci
.where(name: build.name)
.update_all(retried: true)
- project.builds.create!(Hash[attributes])
+ create_build!(attributes)
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def create_build!(attributes)
+ build = project.builds.new(Hash[attributes])
+ build.deployment = ::Gitlab::Ci::Pipeline::Seed::Deployment.new(build).to_resource
+ build.save!
+ build
+ end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index c5cde831964..0aff1bcc8b9 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -11,6 +11,7 @@ module Clusters
configure_provider
create_gitlab_service_account!
configure_kubernetes
+ configure_pre_installed_knative if provider.knative_pre_installed?
cluster.save!
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
log_service_error(e.class.name, provider.id, e.message)
@@ -48,6 +49,13 @@ module Clusters
token: request_kubernetes_token)
end
+ def configure_pre_installed_knative
+ knative = cluster.build_application_knative(
+ hostname: 'example.com'
+ )
+ knative.make_pre_installed!
+ end
+
def request_kubernetes_token
Clusters::Kubernetes::FetchKubernetesTokenService.new(
kube_client,
diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb
index 80040511ec2..7dc2d3c32f1 100644
--- a/app/services/clusters/gcp/provision_service.rb
+++ b/app/services/clusters/gcp/provision_service.rb
@@ -3,6 +3,8 @@
module Clusters
module Gcp
class ProvisionService
+ CLOUD_RUN_ADDONS = %i[http_load_balancing istio_config cloud_run_config].freeze
+
attr_reader :provider
def execute(provider)
@@ -22,13 +24,16 @@ module Clusters
private
def get_operation_id
+ enable_addons = provider.cloud_run? ? CLOUD_RUN_ADDONS : []
+
operation = provider.api_client.projects_zones_clusters_create(
provider.gcp_project_id,
provider.zone,
provider.cluster.name,
provider.num_nodes,
machine_type: provider.machine_type,
- legacy_abac: provider.legacy_abac
+ legacy_abac: provider.legacy_abac,
+ enable_addons: enable_addons
)
unless operation.status == 'PENDING' || operation.status == 'RUNNING'
diff --git a/app/services/concerns/git/change_params.rb b/app/services/concerns/git/change_params.rb
new file mode 100644
index 00000000000..32faf805b5e
--- /dev/null
+++ b/app/services/concerns/git/change_params.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Git
+ module ChangeParams
+ private
+
+ %i[oldrev newrev ref].each do |method|
+ define_method method do
+ change[method]
+ end
+ end
+
+ def change
+ @change ||= params.fetch(:change, {})
+ end
+ end
+end
diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb
new file mode 100644
index 00000000000..2572802e6a1
--- /dev/null
+++ b/app/services/deployments/after_create_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Deployments
+ class AfterCreateService
+ attr_reader :deployment
+ attr_reader :deployable
+
+ delegate :environment, to: :deployment
+ delegate :variables, to: :deployable
+ delegate :options, to: :deployable, allow_nil: true
+
+ def initialize(deployment)
+ @deployment = deployment
+ @deployable = deployment.deployable
+ end
+
+ def execute
+ deployment.create_ref
+ deployment.invalidate_cache
+
+ update_environment(deployment)
+
+ deployment
+ end
+
+ def update_environment(deployment)
+ ActiveRecord::Base.transaction do
+ if (url = expanded_environment_url)
+ environment.external_url = url
+ end
+
+ environment.fire_state_event(action)
+
+ if environment.save && !environment.stopped?
+ deployment.update_merge_request_metrics!
+ end
+ end
+ end
+
+ private
+
+ def environment_options
+ options&.dig(:environment) || {}
+ end
+
+ def expanded_environment_url
+ ExpandVariables.expand(environment_url, -> { variables }) if environment_url
+ end
+
+ def environment_url
+ environment_options[:url]
+ end
+
+ def action
+ environment_options[:action] || 'start'
+ end
+ end
+end
+
+Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb
new file mode 100644
index 00000000000..89e3f7c8b83
--- /dev/null
+++ b/app/services/deployments/create_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Deployments
+ class CreateService
+ attr_reader :environment, :current_user, :params
+
+ def initialize(environment, current_user, params)
+ @environment = environment
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ create_deployment.tap do |deployment|
+ AfterCreateService.new(deployment).execute if deployment.persisted?
+ end
+ end
+
+ def create_deployment
+ environment.deployments.create(deployment_attributes)
+ end
+
+ def deployment_attributes
+ # We use explicit parameters here so we never by accident allow parameters
+ # to be set that one should not be able to set (e.g. the row ID).
+ {
+ cluster_id: environment.deployment_platform&.cluster_id,
+ project_id: environment.project_id,
+ environment_id: environment.id,
+ ref: params[:ref],
+ tag: params[:tag],
+ sha: params[:sha],
+ user: current_user,
+ on_stop: params[:on_stop],
+ status: params[:status]
+ }
+ end
+ end
+end
diff --git a/app/services/deployments/update_service.rb b/app/services/deployments/update_service.rb
new file mode 100644
index 00000000000..7c8215d28f2
--- /dev/null
+++ b/app/services/deployments/update_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Deployments
+ class UpdateService
+ attr_reader :deployment, :params
+
+ def initialize(deployment, params)
+ @deployment = deployment
+ @params = params
+ end
+
+ def execute
+ deployment.update(status: params[:status])
+ end
+ end
+end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 395c5fe09ac..f7282c22a52 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -73,15 +73,27 @@ class EventCreateService
end
def push(project, current_user, push_data)
+ create_push_event(PushEventPayloadService, project, current_user, push_data)
+ end
+
+ def bulk_push(project, current_user, push_data)
+ create_push_event(BulkPushEventPayloadService, project, current_user, push_data)
+ end
+
+ private
+
+ def create_record_event(record, current_user, status)
+ create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
+ end
+
+ def create_push_event(service_class, project, current_user, push_data)
# We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being
# rolled back as well.
event = Event.transaction do
new_event = create_event(project, current_user, Event::PUSHED)
- PushEventPayloadService
- .new(new_event, push_data)
- .execute
+ service_class.new(new_event, push_data).execute
new_event
end
@@ -92,12 +104,6 @@ class EventCreateService
Users::ActivityService.new(current_user, 'push').execute
end
- private
-
- def create_record_event(record, current_user, status)
- create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
- end
-
def create_event(resource_parent, current_user, status, attributes = {})
attributes.reverse_merge!(
action: status,
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 35a4d2015fa..0801fd4d03f 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -3,6 +3,7 @@
module Git
class BaseHooksService < ::BaseService
include Gitlab::Utils::StrongMemoize
+ include ChangeParams
# The N most recent commits to process in a single push payload.
PROCESS_COMMIT_LIMIT = 100
@@ -15,8 +16,6 @@ module Git
# Not a hook, but it needs access to the list of changed commits
enqueue_invalidate_cache
- update_remote_mirrors
-
success
end
@@ -49,6 +48,8 @@ module Git
# Push events in the activity feed only show information for the
# last commit.
def create_events
+ return unless params.fetch(:create_push_event, true)
+
EventCreateService.new.push(project, current_user, event_push_data)
end
@@ -63,6 +64,8 @@ module Git
end
def execute_project_hooks
+ return unless params.fetch(:execute_project_hooks, true)
+
# Creating push_data invokes one CommitDelta RPC per commit. Only
# build this data if we actually need it.
project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
@@ -79,20 +82,20 @@ module Git
def pipeline_params
{
- before: params[:oldrev],
- after: params[:newrev],
- ref: params[:ref],
+ before: oldrev,
+ after: newrev,
+ ref: ref,
push_options: params[:push_options] || {},
checkout_sha: Gitlab::DataBuilder::Push.checkout_sha(
- project.repository, params[:newrev], params[:ref])
+ project.repository, newrev, ref)
}
end
def push_data_params(commits:, with_changed_files: true)
{
- oldrev: params[:oldrev],
- newrev: params[:newrev],
- ref: params[:ref],
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref,
project: project,
user: current_user,
commits: commits,
@@ -121,13 +124,6 @@ module Git
{}
end
- def update_remote_mirrors
- return unless project.has_remote_mirror?
-
- project.mark_stuck_remote_mirrors_as_failed!
- project.update_remote_mirrors
- end
-
def log_pipeline_errors(exception)
data = {
class: self.class.name,
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index c633cff2822..69f1f9eb31f 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -20,15 +20,15 @@ module Git
strong_memoize(:commits) do
if creating_default_branch?
# The most recent PROCESS_COMMIT_LIMIT commits in the default branch
- project.repository.commits(params[:newrev], limit: PROCESS_COMMIT_LIMIT)
+ project.repository.commits(newrev, limit: PROCESS_COMMIT_LIMIT)
elsif creating_branch?
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually
# pushed, but that shouldn't matter because we check for existing
# cross-references later.
- project.repository.commits_between(project.default_branch, params[:newrev])
+ project.repository.commits_between(project.default_branch, newrev)
elsif updating_branch?
- project.repository.commits_between(params[:oldrev], params[:newrev])
+ project.repository.commits_between(oldrev, newrev)
else # removing branch
[]
end
@@ -70,7 +70,7 @@ module Git
def branch_update_hooks
# Update the bare repositories info/attributes file using the contents of
# the default branch's .gitattributes file
- project.repository.copy_gitattributes(params[:ref]) if default_branch?
+ project.repository.copy_gitattributes(ref) if default_branch?
end
def branch_change_hooks
@@ -118,7 +118,7 @@ module Git
# https://gitlab.com/gitlab-org/gitlab-foss/issues/59257
def creating_branch?
strong_memoize(:creating_branch) do
- Gitlab::Git.blank_ref?(params[:oldrev]) ||
+ Gitlab::Git.blank_ref?(oldrev) ||
!project.repository.branch_exists?(branch_name)
end
end
@@ -128,7 +128,7 @@ module Git
end
def removing_branch?
- Gitlab::Git.blank_ref?(params[:newrev])
+ Gitlab::Git.blank_ref?(newrev)
end
def creating_default_branch?
@@ -137,7 +137,7 @@ module Git
def count_commits_in_branch
strong_memoize(:count_commits_in_branch) do
- project.repository.commit_count_for_ref(params[:ref])
+ project.repository.commit_count_for_ref(ref)
end
end
@@ -148,7 +148,7 @@ module Git
end
def branch_name
- strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(ref) }
end
def upstream_commit_ids(commits)
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index 49c54e42b7c..da45bcc7eaa 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -4,6 +4,7 @@ module Git
class BranchPushService < ::BaseService
include Gitlab::Access
include Gitlab::Utils::StrongMemoize
+ include ChangeParams
# This method will be called after each git update
# and only if the provided user and project are present in GitLab.
@@ -19,7 +20,7 @@ module Git
# 6. Checks if the project's main language has changed
#
def execute
- return unless Gitlab::Git.branch_ref?(params[:ref])
+ return unless Gitlab::Git.branch_ref?(ref)
enqueue_update_mrs
enqueue_detect_repository_languages
@@ -38,9 +39,9 @@ module Git
UpdateMergeRequestsWorker.perform_async(
project.id,
current_user.id,
- params[:oldrev],
- params[:newrev],
- params[:ref]
+ oldrev,
+ newrev,
+ ref
)
end
@@ -57,13 +58,6 @@ module Git
Ci::StopEnvironmentsService.new(project, current_user).execute(branch_name)
end
- def update_remote_mirrors
- return unless project.has_remote_mirror?
-
- project.mark_stuck_remote_mirrors_as_failed!
- project.update_remote_mirrors
- end
-
def execute_related_hooks
BranchHooksService.new(project, current_user, params).execute
end
@@ -76,11 +70,11 @@ module Git
end
def removing_branch?
- Gitlab::Git.blank_ref?(params[:newrev])
+ Gitlab::Git.blank_ref?(newrev)
end
def branch_name
- strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(ref) }
end
def default_branch?
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
new file mode 100644
index 00000000000..3052bed51bc
--- /dev/null
+++ b/app/services/git/process_ref_changes_service.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Git
+ class ProcessRefChangesService < BaseService
+ PIPELINE_PROCESS_LIMIT = 4
+
+ def execute
+ changes = params[:changes]
+
+ process_changes_by_action(:branch, changes.branch_changes)
+ process_changes_by_action(:tag, changes.tag_changes)
+ end
+
+ private
+
+ def process_changes_by_action(ref_type, changes)
+ changes_by_action = group_changes_by_action(changes)
+
+ changes_by_action.each do |action, changes|
+ process_changes(ref_type, action, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
+ end
+ end
+
+ def group_changes_by_action(changes)
+ changes.group_by do |change|
+ change_action(change)
+ end
+ end
+
+ def change_action(change)
+ return :created if Gitlab::Git.blank_ref?(change[:oldrev])
+ return :removed if Gitlab::Git.blank_ref?(change[:newrev])
+
+ :pushed
+ end
+
+ def execute_project_hooks?(changes)
+ (changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project)
+ end
+
+ def process_changes(ref_type, action, changes, execute_project_hooks:)
+ push_service_class = push_service_class_for(ref_type)
+
+ create_bulk_push_event = changes.size > Gitlab::CurrentSettings.push_event_activities_limit
+
+ changes.each do |change|
+ push_service_class.new(
+ project,
+ current_user,
+ change: change,
+ push_options: params[:push_options],
+ create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
+ execute_project_hooks: execute_project_hooks,
+ create_push_event: !create_bulk_push_event
+ ).execute
+ end
+
+ create_bulk_push_event(ref_type, action, changes) if create_bulk_push_event
+ end
+
+ def create_bulk_push_event(ref_type, action, changes)
+ EventCreateService.new.bulk_push(
+ project,
+ current_user,
+ Gitlab::DataBuilder::Push.build_bulk(action: action, ref_type: ref_type, changes: changes)
+ )
+ end
+
+ def push_service_class_for(ref_type)
+ return Git::TagPushService if ref_type == :tag
+
+ Git::BranchPushService
+ end
+ end
+end
diff --git a/app/services/git/tag_hooks_service.rb b/app/services/git/tag_hooks_service.rb
index e5b109c79d6..0e5e1bbc992 100644
--- a/app/services/git/tag_hooks_service.rb
+++ b/app/services/git/tag_hooks_service.rb
@@ -18,12 +18,12 @@ module Git
def tag
strong_memoize(:tag) do
- next if Gitlab::Git.blank_ref?(params[:newrev])
+ next if Gitlab::Git.blank_ref?(newrev)
- tag_name = Gitlab::Git.ref_name(params[:ref])
+ tag_name = Gitlab::Git.ref_name(ref)
tag = project.repository.find_tag(tag_name)
- tag if tag && tag.target == params[:newrev]
+ tag if tag && tag.target == newrev
end
end
diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb
index ee4166dccd0..9a266f7d74c 100644
--- a/app/services/git/tag_push_service.rb
+++ b/app/services/git/tag_push_service.rb
@@ -2,8 +2,10 @@
module Git
class TagPushService < ::BaseService
+ include ChangeParams
+
def execute
- return unless Gitlab::Git.tag_ref?(params[:ref])
+ return unless Gitlab::Git.tag_ref?(ref)
project.repository.before_push_tag
TagHooksService.new(project, current_user, params).execute
diff --git a/app/services/grafana/proxy_service.rb b/app/services/grafana/proxy_service.rb
new file mode 100644
index 00000000000..74fcdc750b0
--- /dev/null
+++ b/app/services/grafana/proxy_service.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# Proxies calls to a Grafana-integrated Prometheus instance
+# through the Grafana proxy API
+
+# This allows us to fetch and render metrics in GitLab from a Prometheus
+# instance for which dashboards are configured in Grafana
+module Grafana
+ class ProxyService < BaseService
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(service) { service.cache_key }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ attr_accessor :project, :datasource_id, :proxy_path, :query_params
+
+ # @param project_id [Integer] Project id for which grafana is configured.
+ #
+ # See #initialize for other parameters.
+ def self.from_cache(project_id, datasource_id, proxy_path, query_params)
+ project = Project.find(project_id)
+
+ new(project, datasource_id, proxy_path, query_params)
+ end
+
+ # @param project [Project] Project for which grafana is configured.
+ # @param datasource_id [String] Grafana datasource id for Prometheus instance
+ # @param proxy_path [String] Path to Prometheus endpoint; EX) 'api/v1/query_range'
+ # @param query_params [Hash<String, String>] Supported params: [query, start, end, step]
+ def initialize(project, datasource_id, proxy_path, query_params)
+ @project = project
+ @datasource_id = datasource_id
+ @proxy_path = proxy_path
+ @query_params = query_params
+ end
+
+ def execute
+ return cannot_proxy_response unless client
+
+ with_reactive_cache(*cache_key) { |result| result }
+ end
+
+ def calculate_reactive_cache(*)
+ return cannot_proxy_response unless client
+
+ response = client.proxy_datasource(
+ datasource_id: datasource_id,
+ proxy_path: proxy_path,
+ query: query_params
+ )
+
+ success(http_status: response.code, body: response.body)
+ rescue ::Grafana::Client::Error => error
+ service_unavailable_response(error)
+ end
+
+ # Required for ReactiveCaching; Usage overridden by
+ # self.reactive_cache_worker_finder
+ def id
+ nil
+ end
+
+ def cache_key
+ [project.id, datasource_id, proxy_path, query_params]
+ end
+
+ private
+
+ def client
+ project.grafana_integration&.client
+ end
+
+ def service_unavailable_response(exception)
+ error(exception.message, :service_unavailable)
+ end
+
+ def cannot_proxy_response
+ error('Proxy support for this API is not available currently')
+ end
+ end
+end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 61bd50616b8..8cc31200689 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -9,6 +9,7 @@ module Groups
def execute
remove_unallowed_params
+ set_visibility_level
@group = Group.new(params)
@@ -68,6 +69,12 @@ module Groups
true
end
+
+ def set_visibility_level
+ return if visibility_level.present?
+
+ params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility
+ end
end
end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index fe7e07ef9f0..6902b7bd529 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -7,7 +7,8 @@ module Groups
namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'),
group_is_already_root: s_('TransferGroup|Group is already a root group.'),
same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'),
- invalid_policies: s_("TransferGroup|You don't have enough permissions.")
+ invalid_policies: s_("TransferGroup|You don't have enough permissions."),
+ group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.')
}.freeze
TransferError = Class.new(StandardError)
@@ -46,6 +47,7 @@ module Groups
raise_transfer_error(:same_parent_as_current) if same_parent?
raise_transfer_error(:invalid_policies) unless valid_policies?
raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
+ raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
end
def group_is_already_root?
@@ -72,6 +74,10 @@ module Groups
end
# rubocop: enable CodeReuse/ActiveRecord
+ def group_projects_contain_registry_images?
+ @group.has_container_repositories?
+ end
+
def update_group_attributes
if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level
update_children_and_projects_visibility
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 534de601e20..be7502a193e 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -8,6 +8,11 @@ module Groups
reject_parent_id!
remove_unallowed_params
+ if renaming_group_with_container_registry_images?
+ group.errors.add(:base, container_images_error)
+ return false
+ end
+
return false unless valid_visibility_level_change?(group, params[:visibility_level])
return false unless valid_share_with_group_lock_change?
@@ -35,6 +40,17 @@ module Groups
# overridden in EE
end
+ def renaming_group_with_container_registry_images?
+ new_path = params[:path]
+
+ new_path && new_path != group.path &&
+ group.has_container_repositories?
+ end
+
+ def container_images_error
+ s_("GroupSettings|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.")
+ end
+
def after_update
if group.previous_changes.include?(:visibility_level) && group.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 3ecb51b60d0..66ac7dac4ca 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -12,16 +12,20 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- clean_up_export_object_files
-
- break unless File.directory?(path)
-
- clean_up_export_files
+ execute_cleanup
end
end
private
+ def execute_cleanup
+ clean_up_export_object_files
+ ensure
+ # We don't want a failure in cleaning up object storage from
+ # blocking us from cleaning up temporary storage.
+ clean_up_export_files if File.directory?(path)
+ end
+
def clean_up_export_files
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end
diff --git a/app/services/issuable/clone/content_rewriter.rb b/app/services/issuable/clone/content_rewriter.rb
index f75b51c4be3..67d2f9fd3fe 100644
--- a/app/services/issuable/clone/content_rewriter.rb
+++ b/app/services/issuable/clone/content_rewriter.rb
@@ -39,6 +39,10 @@ module Issuable
if note.system_note_metadata
new_params[:system_note_metadata] = note.system_note_metadata.dup
+
+ # TODO: Implement copying of description versions when an issue is moved
+ # https://gitlab.com/gitlab-org/gitlab/issues/32300
+ new_params[:system_note_metadata].description_version = nil
end
new_note.update(new_params)
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 805721212ba..14585c2850b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -4,7 +4,7 @@ module Issues
class CloseService < Issues::BaseService
# Closes the supplied issue if the current user is able to do so.
def execute(issue, commit: nil, notifications: true, system_note: true)
- return issue unless can?(current_user, :update_issue, issue)
+ return issue unless can?(current_user, :update_issue, issue) || issue.is_a?(ExternalIssue)
close_issue(issue,
closed_via: commit,
@@ -18,7 +18,7 @@ module Issues
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
def close_issue(issue, closed_via: nil, notifications: true, system_note: true)
- if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue)
+ if project.jira_tracker_active? && issue.is_a?(ExternalIssue)
project.jira_service.close_issue(closed_via, issue)
todo_service.close_issue(issue, current_user)
return issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index dc3c363f650..528b1ea61b3 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -56,7 +56,7 @@ module Issues
handle_milestone_change(issue)
- added_mentions = issue.mentioned_users - old_mentioned_users
+ added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index a061ab22875..561c86475e5 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -10,6 +10,7 @@ module Issues
def add_link(link)
if can_add_link? && (link = parse_link(link))
+ track_meeting_added_event
success(_('Zoom meeting added'), append_to_description(link))
else
error(_('Failed to add a Zoom meeting'))
@@ -17,11 +18,12 @@ module Issues
end
def can_add_link?
- available? && !link_in_issue_description?
+ can? && !link_in_issue_description?
end
def remove_link
if can_remove_link?
+ track_meeting_removed_event
success(_('Zoom meeting removed'), remove_from_description)
else
error(_('Failed to remove a Zoom meeting'))
@@ -29,7 +31,7 @@ module Issues
end
def can_remove_link?
- available? && link_in_issue_description?
+ can? && link_in_issue_description?
end
def parse_link(link)
@@ -44,6 +46,14 @@ module Issues
issue.description || ''
end
+ def track_meeting_added_event
+ ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
+ end
+
+ def track_meeting_removed_event
+ ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
+ end
+
def success(message, description)
ServiceResponse
.success(message: message, payload: { description: description })
@@ -75,14 +85,6 @@ module Issues
issue_description[/(\S+)\z/, 1]
end
- def available?
- feature_enabled? && can?
- end
-
- def feature_enabled?
- Feature.enabled?(:issue_zoom_integration, project)
- end
-
def can?
current_user.can?(:update_issue, project)
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index fbe6c48ac28..0364c0dd479 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -29,9 +29,7 @@ module MergeRequests
closed_issues = merge_request.visible_closing_issues_for(current_user)
closed_issues.each do |issue|
- if can?(current_user, :update_issue, issue)
- Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
- end
+ Issues::CloseService.new(project, current_user).execute(issue, commit: merge_request)
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index edcfc3bf33f..b32499629ff 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -25,6 +25,7 @@ module MergeRequests
outdate_suggestions
refresh_pipelines_on_merge_requests
abort_auto_merges
+ abort_ff_merge_requests_with_when_pipeline_succeeds
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -148,6 +149,31 @@ module MergeRequests
end
end
+ def abort_ff_merge_requests_with_when_pipeline_succeeds
+ return unless @project.ff_merge_must_be_possible?
+
+ requests_with_auto_merge_enabled_to(@push.branch_name).each do |merge_request|
+ next unless merge_request.should_be_rebased?
+
+ abort_auto_merge_with_todo(merge_request, 'target branch was updated')
+ end
+ end
+
+ def abort_auto_merge_with_todo(merge_request, reason)
+ response = abort_auto_merge(merge_request, reason)
+ response = ServiceResponse.new(response)
+ return unless response.success?
+
+ todo_service.merge_request_became_unmergeable(merge_request)
+ end
+
+ def requests_with_auto_merge_enabled_to(target_branch)
+ @project
+ .merge_requests
+ .by_target_branch(target_branch)
+ .with_open_merge_when_pipeline_succeeds
+ end
+
def mark_pending_todos_done
merge_requests_for_source_branch.each do |merge_request|
todo_service.merge_request_push(merge_request, @current_user)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 4acc3f1981a..ae678d4c036 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -69,7 +69,8 @@ module MergeRequests
)
end
- added_mentions = merge_request.mentioned_users - old_mentioned_users
+ added_mentions = merge_request.mentioned_users(current_user) - old_mentioned_users
+
if added_mentions.present?
notification_service.async.new_mentions_in_merge_request(
merge_request,
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
index 60a68568833..6fe14939aaa 100644
--- a/app/services/note_summary.rb
+++ b/app/services/note_summary.rb
@@ -10,6 +10,10 @@ class NoteSummary
project: project, author: author, note: body }
@metadata = { action: action, commit_count: commit_count }.compact
+ if action == 'description' && noteable.saved_description_version
+ @metadata[:description_version] = noteable.saved_description_version
+ end
+
set_commit_params if note[:noteable].is_a?(Commit)
end
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 076df10bf6f..7e6568b5b25 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -50,7 +50,7 @@ module Notes
return if update_params.empty?
return unless supported?(note)
- self.class.noteable_update_service(note).new(note.parent, current_user, update_params).execute(note.noteable)
+ self.class.noteable_update_service(note).new(note.resource_parent, current_user, update_params).execute(note.noteable)
end
end
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 853faed9d85..573be8fbe8b 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -5,7 +5,7 @@ module Notes
def execute(note)
return note unless note.editable?
- old_mentioned_users = note.mentioned_users.to_a
+ old_mentioned_users = note.mentioned_users(current_user).to_a
note.update(params.merge(updated_by: current_user))
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index fca64270cae..9afbb678f5d 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -28,6 +28,10 @@ module NotificationRecipientService
Builder::ProjectMaintainers.new(*args).notification_recipients
end
+ def self.build_new_release_recipients(*args)
+ Builder::NewRelease.new(*args).notification_recipients
+ end
+
module Builder
class Base
def initialize(*)
@@ -359,6 +363,26 @@ module NotificationRecipientService
end
end
+ class NewRelease < Base
+ attr_reader :target
+
+ def initialize(target)
+ @target = target
+ end
+
+ def build!
+ add_recipients(target.project.authorized_users, :custom, nil)
+ end
+
+ def custom_action
+ :new_release
+ end
+
+ def acting_user
+ target.author
+ end
+ end
+
class MergeRequestUnmergeable < Base
attr_reader :target
def initialize(merge_request)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ed357aa0392..b56b2cf14e3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -289,6 +289,15 @@ class NotificationService
end
end
+ # Notify users when a new release is created
+ def send_new_release_notifications(release)
+ recipients = NotificationRecipientService.build_new_release_recipients(release)
+
+ recipients.each do |recipient|
+ mailer.new_release_email(recipient.user.id, release, recipient.reason).deliver_later
+ end
+ end
+
# Members
def new_access_request(member)
return true unless member.notifiable?(:subscription)
diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb
index e30da0f26df..6fc15db9b4c 100644
--- a/app/services/projects/after_import_service.rb
+++ b/app/services/projects/after_import_service.rb
@@ -9,9 +9,16 @@ module Projects
end
def execute
- Projects::HousekeepingService.new(@project).execute do
+ service = Projects::HousekeepingService.new(@project)
+
+ service.execute do
repository.delete_all_refs_except(RESERVED_REF_PREFIXES)
end
+
+ # Right now we don't actually have a way to know if a project
+ # import actually changed, so we increment the counter to avoid
+ # causing GC to run every time.
+ service.increment!
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info( # rubocop:disable Gitlab/RailsLogger
"Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}")
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index d1d9b9f22e8..1b880a7aab1 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -40,7 +40,7 @@ module Projects
return unless tags.count == other_tags.count
# delete all tags
- tags.map(&:delete)
+ tags.map(&:unsafe_delete)
end
def group_by_digest(tags)
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
new file mode 100644
index 00000000000..5129e2269a8
--- /dev/null
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ class DeleteTagsService < BaseService
+ def execute(container_repository)
+ return error('access denied') unless can?(current_user, :destroy_container_image, project)
+
+ tag_names = params[:tags]
+ return error('not tags specified') if tag_names.blank?
+
+ if can_use?
+ smart_delete(container_repository, tag_names)
+ else
+ unsafe_delete(container_repository, tag_names)
+ end
+ end
+
+ private
+
+ def unsafe_delete(container_repository, tag_names)
+ deleted_tags = tag_names.select do |tag_name|
+ container_repository.tag(tag_name).unsafe_delete
+ end
+
+ return error('could not delete tags') if deleted_tags.empty?
+
+ success(deleted: deleted_tags)
+ end
+
+ # Replace a tag on the registry with a dummy tag.
+ # This is a hack as the registry doesn't support deleting individual
+ # tags. This code effectively pushes a dummy image and assigns the tag to it.
+ # This way when the tag is deleted only the dummy image is affected.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
+ def smart_delete(container_repository, tag_names)
+ # generates the blobs for the dummy image
+ dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
+
+ # update the manifests of the tags with the new dummy image
+ tag_digests = tag_names.map do |name|
+ container_repository.client.put_tag(container_repository.path, name, dummy_manifest)
+ end
+
+ # make sure the digests are the same (it should always be)
+ tag_digests.uniq!
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ Gitlab::Sentry.track_exception(ArgumentError.new('multiple tag digests')) if tag_digests.many?
+
+ # Deletes the dummy image
+ # All created tag digests are the same since they all have the same dummy image.
+ # a single delete is sufficient to remove all tags with it
+ if container_repository.delete_tag_by_digest(tag_digests.first)
+ success(deleted: tag_names)
+ else
+ error('could not delete tags')
+ end
+ end
+
+ def can_use?
+ Feature.enabled?(:container_registry_smart_delete, project, default_enabled: true)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
index 91ece024e13..a207fd2c574 100644
--- a/app/services/projects/create_from_template_service.rb
+++ b/app/services/projects/create_from_template_service.rb
@@ -4,8 +4,11 @@ module Projects
class CreateFromTemplateService < BaseService
include Gitlab::Utils::StrongMemoize
+ attr_reader :template_name
+
def initialize(user, params)
@current_user, @params = user, params.to_h.dup
+ @template_name = @params.delete(:template_name).presence
end
def execute
@@ -21,12 +24,6 @@ module Projects
file&.close
end
- def template_name
- strong_memoize(:template_name) do
- params.delete(:template_name).presence
- end
- end
-
private
def validate_template!
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 728eb039b54..ef06545b27d 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -13,7 +13,7 @@ module Projects
end
def execute
- if @params[:template_name].present?
+ if create_from_template?
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
@@ -184,6 +184,10 @@ module Projects
private
+ def create_from_template?
+ @params[:template_name].present? || @params[:template_project_id].present?
+ end
+
def import_schedule
if @project.errors.empty?
@project.import_state.schedule if @project.import? && !@project.bare_repository_import?
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 5fdf98c3c5e..90e703e7050 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -123,11 +123,9 @@ module Projects
mv_repository(old_path, new_path)
end
- # rubocop: disable CodeReuse/ActiveRecord
def repo_exists?(path)
- gitlab_shell.exists?(project.repository_storage, path + '.git')
+ gitlab_shell.repository_exists?(project.repository_storage, path + '.git')
end
- # rubocop: enable CodeReuse/ActiveRecord
def mv_repository(from_path, to_path)
return true unless repo_exists?(from_path)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 17686b45900..47ab7f9a8a0 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -43,6 +43,7 @@ module Projects
shared_runners_enabled: @project.shared_runners_enabled,
namespace_id: target_namespace.id,
fork_network: fork_network,
+ ci_config_path: @project.ci_config_path,
# We need to set ci_default_git_depth to 0 for the forked project when
# @project.ci_default_git_depth is nil in order to keep the same behaviour
# and not get ProjectCiCdSetting::DEFAULT_GIT_DEPTH set on create
diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb
index f97a28b8c3b..b7e9d3e8791 100644
--- a/app/services/projects/hashed_storage/base_repository_service.rb
+++ b/app/services/projects/hashed_storage/base_repository_service.rb
@@ -20,16 +20,13 @@ module Projects
protected
- # rubocop: disable CodeReuse/ActiveRecord
def has_wiki?
- gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
+ gitlab_shell.repository_exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
+ from_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{from_name}.git")
+ to_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{to_name}.git")
# If we don't find the repository on either original or target we should log that as it could be an issue if the
# project was not originally empty.
@@ -46,7 +43,6 @@ module Projects
gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
- # rubocop: enable CodeReuse/ActiveRecord
def rollback_folder_move
move_repository(new_disk_path, old_disk_path)
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index e248a13c702..0a0bd90cd20 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -8,7 +8,6 @@ module Projects
@old_storage_version = project.storage_version
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
- project.ensure_storage_path_exists
@new_disk_path = project.disk_path
diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb
index 67733f4770b..a705112ebe3 100644
--- a/app/services/projects/hashed_storage/rollback_repository_service.rb
+++ b/app/services/projects/hashed_storage/rollback_repository_service.rb
@@ -8,7 +8,6 @@ module Projects
@old_storage_version = project.storage_version
project.storage_version = nil
- project.ensure_storage_path_exists
@new_disk_path = project.disk_path
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 9c6d7ef41f6..d3638c57552 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -12,6 +12,8 @@ module Projects
private
+ attr_accessor :shared
+
def execute_after_export_action(after_export_strategy)
return unless after_export_strategy
@@ -21,50 +23,54 @@ module Projects
end
def save_all!
- if save_services
- Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
+ if save_exporters
+ Gitlab::ImportExport::Saver.save(project: project, shared: shared)
notify_success
else
cleanup_and_notify_error!
end
end
- def save_services
- [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
+ def save_exporters
+ exporters.all?(&:save)
+ end
+
+ def exporters
+ [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver]
end
def version_saver
- Gitlab::ImportExport::VersionSaver.new(shared: @shared)
+ Gitlab::ImportExport::VersionSaver.new(shared: shared)
end
def avatar_saver
- Gitlab::ImportExport::AvatarSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::AvatarSaver.new(project: project, shared: shared)
end
def project_tree_saver
- Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared, params: @params)
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: current_user, shared: shared, params: params)
end
def uploads_saver
- Gitlab::ImportExport::UploadsSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::UploadsSaver.new(project: project, shared: shared)
end
def repo_saver
- Gitlab::ImportExport::RepoSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::RepoSaver.new(project: project, shared: shared)
end
def wiki_repo_saver
- Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: shared)
end
def lfs_saver
- Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
end
def cleanup_and_notify_error
- Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger
+ Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger
- FileUtils.rm_rf(@shared.export_path)
+ FileUtils.rm_rf(shared.export_path)
notify_error
end
@@ -72,7 +78,7 @@ module Projects
def cleanup_and_notify_error!
cleanup_and_notify_error
- raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
+ raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
end
def notify_success
@@ -80,8 +86,10 @@ module Projects
end
def notify_error
- notification_service.project_not_exported(@project, @current_user, @shared.errors)
+ notification_service.project_not_exported(project, current_user, shared.errors)
end
end
end
end
+
+Projects::ImportExport::ExportService.prepend_if_ee('EE::Projects::ImportExport::ExportService')
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index dd72c2844c2..64519501ff4 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -12,7 +12,9 @@ module Projects
private
def project_update_params
- error_tracking_params.merge(metrics_setting_params)
+ error_tracking_params
+ .merge(metrics_setting_params)
+ .merge(grafana_integration_params)
end
def metrics_setting_params
@@ -44,6 +46,14 @@ module Projects
}
}
end
+
+ def grafana_integration_params
+ return {} unless attrs = params[:grafana_integration_attributes]
+
+ destroy = attrs[:grafana_url].blank? && attrs[:token].blank?
+
+ { grafana_integration_attributes: attrs.merge(_destroy: destroy) }
+ end
end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index fa7a4f0ed82..e8a87fc4320 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -53,6 +53,7 @@ module Projects
def success
@status.success
+ @project.mark_pages_as_deployed
super
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 7c6c6878400..e686d3bf7c2 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -1,17 +1,9 @@
# frozen_string_literal: true
module Search
- class SnippetService
- attr_accessor :current_user, :params
-
- def initialize(user, params)
- @current_user, @params = user, params.dup
- end
-
+ class SnippetService < Search::GlobalService
def execute
- snippets = SnippetsFinder.new(current_user).execute
-
- Gitlab::SnippetSearchResults.new(snippets, params[:search])
+ Gitlab::SnippetSearchResults.new(current_user, params[:search])
end
def scope
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index f2f133dae28..babe69cfdc8 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -37,7 +37,8 @@ class SpamService
else
# Otherwise, it goes to Akismet and check if it's a spam. If that's the
# case, it assigns spammable record as "spam" and create a SpamLog record.
- spammable.spam = check(api)
+ possible_spam = check(api)
+ spammable.spam = possible_spam unless spammable.allow_possible_spam?
spammable.spam_log = spam_log
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index c01094bd689..b3eee01ea7a 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -16,20 +16,9 @@ module SystemNoteService
# existing_commits - Array of Commits added in a previous push
# oldrev - Optional String SHA of a previous Commit
#
- # See new_commit_summary and existing_commit_summary.
- #
# Returns the created Note object
def add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil)
- total_count = new_commits.length + existing_commits.length
- commits_text = "#{total_count} commit".pluralize(total_count)
-
- text_parts = ["added #{commits_text}"]
- text_parts << commits_list(noteable, new_commits, existing_commits, oldrev)
- text_parts << "[Compare with previous version](#{diff_comparison_path(noteable, project, oldrev)})"
-
- body = text_parts.join("\n\n")
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
+ ::SystemNotes::CommitService.new(noteable: noteable, project: project, author: author).add_commits(new_commits, existing_commits, oldrev)
end
# Called when a commit was tagged
@@ -41,84 +30,19 @@ module SystemNoteService
#
# Returns the created Note object
def tag_commit(noteable, project, author, tag_name)
- link = url_helpers.project_tag_path(project, id: tag_name)
- body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'tag'))
+ ::SystemNotes::CommitService.new(noteable: noteable, project: project, author: author).tag_commit(tag_name)
end
- # Called when the assignee of a Noteable is changed or removed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # assignee - User being assigned, or nil
- #
- # Example Note text:
- #
- # "removed assignee"
- #
- # "assigned to @rspeicher"
- #
- # Returns the created Note object
def change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_assignee(assignee)
end
- # Called when the assignees of an issuable is changed or removed
- #
- # issuable - Issuable object (responds to assignees)
- # project - Project owning noteable
- # author - User performing the change
- # assignees - Users being assigned, or nil
- #
- # Example Note text:
- #
- # "removed all assignees"
- #
- # "assigned to @user1 additionally to @user2"
- #
- # "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
- #
- # "assigned to @user1 and @user2"
- #
- # Returns the created Note object
def change_issuable_assignees(issuable, project, author, old_assignees)
- unassigned_users = old_assignees - issuable.assignees
- added_users = issuable.assignees.to_a - old_assignees
- text_parts = []
-
- Gitlab::I18n.with_default_locale do
- text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
- text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
- end
-
- body = text_parts.join(' and ')
-
- create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee'))
+ ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_assignees(old_assignees)
end
- # Called when the milestone of a Noteable is changed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # milestone - Milestone being assigned, or nil
- #
- # Example Note text:
- #
- # "removed milestone"
- #
- # "changed milestone to 7.11"
- #
- # Returns the created Note object
def change_milestone(noteable, project, author, milestone)
- format = milestone&.group_milestone? ? :name : :iid
- body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_milestone(milestone)
end
# Called when the due_date of a Noteable is changed
@@ -198,28 +122,8 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
- # Called when the status of a Noteable is changed
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # status - String status
- # source - Mentionable performing the change, or nil
- #
- # Example Note text:
- #
- # "merged"
- #
- # "closed via bc17db76"
- #
- # Returns the created Note object
def change_status(noteable, project, author, status, source = nil)
- body = status.dup
- body << " via #{source.gfm_reference(project)}" if source
-
- action = status == 'reopened' ? 'opened' : status
-
- create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_status(status, source)
end
# Called when 'merge when pipeline succeeds' is executed
@@ -302,69 +206,16 @@ module SystemNoteService
note
end
- # Called when the title of a Noteable is changed
- #
- # noteable - Noteable object that responds to `title`
- # project - Project owning noteable
- # author - User performing the change
- # old_title - Previous String title
- #
- # Example Note text:
- #
- # "changed title from **Old** to **New**"
- #
- # Returns the created Note object
def change_title(noteable, project, author, old_title)
- new_title = noteable.title.dup
-
- old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
-
- marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs, mode: :deletion)
- marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs, mode: :addition)
-
- body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_title(old_title)
end
- # Called when the description of a Noteable is changed
- #
- # noteable - Noteable object that responds to `description`
- # project - Project owning noteable
- # author - User performing the change
- #
- # Example Note text:
- #
- # "changed the description"
- #
- # Returns the created Note object
def change_description(noteable, project, author)
- body = 'changed the description'
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_description
end
- # Called when the confidentiality changes
- #
- # issue - Issue object
- # project - Project owning the issue
- # author - User performing the change
- #
- # Example Note text:
- #
- # "made the issue confidential"
- #
- # Returns the created Note object
def change_issue_confidentiality(issue, project, author)
- if issue.confidential
- body = 'made the issue confidential'
- action = 'confidential'
- else
- body = 'made the issue visible to everyone'
- action = 'visible'
- end
-
- create_note(NoteSummary.new(issue, project, author, body, action: action))
+ ::SystemNotes::IssuablesService.new(noteable: issue, project: project, author: author).change_issue_confidentiality
end
# Called when a branch in Noteable is changed
@@ -433,195 +284,48 @@ module SystemNoteService
create_note(NoteSummary.new(issue, project, author, body, action: 'merge'))
end
- # Called when a Mentionable references a Noteable
- #
- # noteable - Noteable object being referenced
- # mentioner - Mentionable object
- # author - User performing the reference
- #
- # Example Note text:
- #
- # "mentioned in #1"
- #
- # "mentioned in !2"
- #
- # "mentioned in 54f7727c"
- #
- # See cross_reference_note_content.
- #
- # Returns the created Note object
def cross_reference(noteable, mentioner, author)
- return if cross_reference_disallowed?(noteable, mentioner)
-
- gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
- body = cross_reference_note_content(gfm_reference)
-
- if noteable.is_a?(ExternalIssue)
- noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
- else
- create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
- end
+ ::SystemNotes::IssuablesService.new(noteable: noteable, author: author).cross_reference(mentioner)
end
- # Check if a cross-reference is disallowed
- #
- # This method prevents adding a "mentioned in !1" note on every single commit
- # in a merge request. Additionally, it prevents the creation of references to
- # external issues (which would fail).
- #
- # noteable - Noteable object being referenced
- # mentioner - Mentionable object
- #
- # Returns Boolean
- def cross_reference_disallowed?(noteable, mentioner)
- return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
- return false unless mentioner.is_a?(MergeRequest)
- return false unless noteable.is_a?(Commit)
-
- mentioner.commits.include?(noteable)
- end
-
- # Check if a cross reference to a noteable from a mentioner already exists
- #
- # This method is used to prevent multiple notes being created for a mention
- # when a issue is updated, for example. The method also calls notes_for_mentioner
- # to check if the mentioner is a commit, and return matches only on commit hash
- # instead of project + commit, to avoid repeated mentions from forks.
- #
- # noteable - Noteable object being referenced
- # mentioner - Mentionable object
- #
- # Returns Boolean
def cross_reference_exists?(noteable, mentioner)
- notes = noteable.notes.system
- notes_for_mentioner(mentioner, noteable, notes).exists?
- end
-
- # Build an Array of lines detailing each commit added in a merge request
- #
- # new_commits - Array of new Commit objects
- #
- # Returns an Array of Strings
- def new_commit_summary(new_commits)
- new_commits.collect do |commit|
- content_tag('li', "#{commit.short_id} - #{commit.title}")
- end
+ ::SystemNotes::IssuablesService.new(noteable: noteable).cross_reference_exists?(mentioner)
end
- # Called when the status of a Task has changed
- #
- # noteable - Noteable object.
- # project - Project owning noteable
- # author - User performing the change
- # new_task - TaskList::Item object.
- #
- # Example Note text:
- #
- # "marked the task Whatever as completed."
- #
- # Returns the created Note object
def change_task_status(noteable, project, author, new_task)
- status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
- body = "marked the task **#{new_task.source}** as #{status_label}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_task_status(new_task)
end
- # Called when noteable has been moved to another project
- #
- # direction - symbol, :to or :from
- # noteable - Noteable object
- # noteable_ref - Referenced noteable
- # author - User performing the move
- #
- # Example Note text:
- #
- # "moved to some_namespace/project_new#11"
- #
- # Returns the created Note object
def noteable_moved(noteable, project, noteable_ref, author, direction:)
- unless [:to, :from].include?(direction)
- raise ArgumentError, "Invalid direction `#{direction}`"
- end
-
- cross_reference = noteable_ref.to_reference(project)
- body = "moved #{direction} #{cross_reference}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).noteable_moved(noteable_ref, direction)
end
- # Called when a Noteable has been marked as a duplicate of another Issue
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # canonical_issue - Issue that this is a duplicate of
- #
- # Example Note text:
- #
- # "marked this issue as a duplicate of #1234"
- #
- # "marked this issue as a duplicate of other_project#5678"
- #
- # Returns the created Note object
def mark_duplicate_issue(noteable, project, author, canonical_issue)
- body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
- create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).mark_duplicate_issue(canonical_issue)
end
- # Called when a Noteable has been marked as the canonical Issue of a duplicate
- #
- # noteable - Noteable object
- # project - Project owning noteable
- # author - User performing the change
- # duplicate_issue - Issue that was a duplicate of this
- #
- # Example Note text:
- #
- # "marked #1234 as a duplicate of this issue"
- #
- # "marked other_project#5678 as a duplicate of this issue"
- #
- # Returns the created Note object
def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
- body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
- create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).mark_canonical_issue_of_duplicate(duplicate_issue)
end
def discussion_lock(issuable, author)
- action = issuable.discussion_locked? ? 'locked' : 'unlocked'
- body = "#{action} this #{issuable.class.to_s.titleize.downcase}"
-
- create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
+ ::SystemNotes::IssuablesService.new(noteable: issuable, project: issuable.project, author: author).discussion_lock
end
- def cross_reference?(note_text)
- note_text =~ /\A#{cross_reference_note_prefix}/i
+ def cross_reference_disallowed?(noteable, mentioner)
+ ::SystemNotes::IssuablesService.new(noteable: noteable).cross_reference_disallowed?(mentioner)
end
def zoom_link_added(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
+ ::SystemNotes::ZoomService.new(noteable: issue, project: project, author: author).zoom_link_added
end
def zoom_link_removed(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
+ ::SystemNotes::ZoomService.new(noteable: issue, project: project, author: author).zoom_link_removed
end
private
- # rubocop: disable CodeReuse/ActiveRecord
- def notes_for_mentioner(mentioner, noteable, notes)
- if mentioner.is_a?(Commit)
- text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
- notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
- else
- gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
- text = cross_reference_note_content(gfm_reference)
- notes.where(note: [text, text.capitalize])
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def create_note(note_summary)
note = Note.create(note_summary.note.merge(system: true))
note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
@@ -629,79 +333,10 @@ module SystemNoteService
note
end
- def cross_reference_note_prefix
- 'mentioned in '
- end
-
- def cross_reference_note_content(gfm_reference)
- "#{cross_reference_note_prefix}#{gfm_reference}"
- end
-
- # Builds a list of existing and new commits according to existing_commits and
- # new_commits methods.
- # Returns a String wrapped in `ul` and `li` tags.
- def commits_list(noteable, new_commits, existing_commits, oldrev)
- existing_commit_summary = existing_commit_summary(noteable, existing_commits, oldrev)
- new_commit_summary = new_commit_summary(new_commits).join
-
- content_tag('ul', "#{existing_commit_summary}#{new_commit_summary}".html_safe)
- end
-
- # Build a single line summarizing existing commits being added in a merge
- # request
- #
- # noteable - MergeRequest object
- # existing_commits - Array of existing Commit objects
- # oldrev - Optional String SHA of a previous Commit
- #
- # Examples:
- #
- # "* ea0f8418...2f4426b7 - 24 commits from branch `master`"
- #
- # "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`"
- #
- # "* ea0f8418 - 1 commit from branch `feature`"
- #
- # Returns a newline-terminated String
- def existing_commit_summary(noteable, existing_commits, oldrev = nil)
- return '' if existing_commits.empty?
-
- count = existing_commits.size
-
- commit_ids = if count == 1
- existing_commits.first.short_id
- else
- if oldrev && !Gitlab::Git.blank_ref?(oldrev)
- "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
- else
- "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
- end
- end
-
- commits_text = "#{count} commit".pluralize(count)
-
- branch = noteable.target_branch
- branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork?
-
- branch_name = content_tag('code', branch)
- content_tag('li', "#{commit_ids} - #{commits_text} from branch #{branch_name}".html_safe)
- end
-
def url_helpers
@url_helpers ||= Gitlab::Routing.url_helpers
end
- def diff_comparison_path(merge_request, project, oldrev)
- diff_id = merge_request.merge_request_diff.id
-
- url_helpers.diffs_project_merge_request_path(
- project,
- merge_request,
- diff_id: diff_id,
- start_sha: oldrev
- )
- end
-
def content_tag(*args)
ActionController::Base.helpers.content_tag(*args)
end
diff --git a/app/services/system_notes/base_service.rb b/app/services/system_notes/base_service.rb
new file mode 100644
index 00000000000..7341a25b133
--- /dev/null
+++ b/app/services/system_notes/base_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class BaseService
+ attr_reader :noteable, :project, :author
+
+ def initialize(noteable: nil, author: nil, project: nil)
+ @noteable = noteable
+ @project = project
+ @author = author
+ end
+
+ protected
+
+ def create_note(note_summary)
+ note = Note.create(note_summary.note.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
+
+ note
+ end
+
+ def content_tag(*args)
+ ActionController::Base.helpers.content_tag(*args)
+ end
+
+ def url_helpers
+ @url_helpers ||= Gitlab::Routing.url_helpers
+ end
+ end
+end
diff --git a/app/services/system_notes/commit_service.rb b/app/services/system_notes/commit_service.rb
new file mode 100644
index 00000000000..11119956e0f
--- /dev/null
+++ b/app/services/system_notes/commit_service.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class CommitService < ::SystemNotes::BaseService
+ # Called when commits are added to a Merge Request
+ #
+ # new_commits - Array of Commits added since last push
+ # existing_commits - Array of Commits added in a previous push
+ # oldrev - Optional String SHA of a previous Commit
+ #
+ # See new_commit_summary and existing_commit_summary.
+ #
+ # Returns the created Note object
+ def add_commits(new_commits, existing_commits = [], oldrev = nil)
+ total_count = new_commits.length + existing_commits.length
+ commits_text = "#{total_count} commit".pluralize(total_count)
+
+ text_parts = ["added #{commits_text}"]
+ text_parts << commits_list(noteable, new_commits, existing_commits, oldrev)
+ text_parts << "[Compare with previous version](#{diff_comparison_path(noteable, project, oldrev)})"
+
+ body = text_parts.join("\n\n")
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
+ end
+
+ # Called when a commit was tagged
+ #
+ # tag_name - The created tag name
+ #
+ # Returns the created Note object
+ def tag_commit(tag_name)
+ link = url_helpers.project_tag_path(project, id: tag_name)
+ body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'tag'))
+ end
+
+ # Build an Array of lines detailing each commit added in a merge request
+ #
+ # new_commits - Array of new Commit objects
+ #
+ # Returns an Array of Strings
+ def new_commit_summary(new_commits)
+ new_commits.collect do |commit|
+ content_tag('li', "#{commit.short_id} - #{commit.title}")
+ end
+ end
+
+ private
+
+ # Builds a list of existing and new commits according to existing_commits and
+ # new_commits methods.
+ # Returns a String wrapped in `ul` and `li` tags.
+ def commits_list(noteable, new_commits, existing_commits, oldrev)
+ existing_commit_summary = existing_commit_summary(noteable, existing_commits, oldrev)
+ new_commit_summary = new_commit_summary(new_commits).join
+
+ content_tag('ul', "#{existing_commit_summary}#{new_commit_summary}".html_safe)
+ end
+
+ # Build a single line summarizing existing commits being added in a merge
+ # request
+ #
+ # existing_commits - Array of existing Commit objects
+ # oldrev - Optional String SHA of a previous Commit
+ #
+ # Examples:
+ #
+ # "* ea0f8418...2f4426b7 - 24 commits from branch `master`"
+ #
+ # "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`"
+ #
+ # "* ea0f8418 - 1 commit from branch `feature`"
+ #
+ # Returns a newline-terminated String
+ def existing_commit_summary(noteable, existing_commits, oldrev = nil)
+ return '' if existing_commits.empty?
+
+ count = existing_commits.size
+
+ commit_ids = if count == 1
+ existing_commits.first.short_id
+ else
+ if oldrev && !Gitlab::Git.blank_ref?(oldrev)
+ "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
+ else
+ "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
+ end
+ end
+
+ commits_text = "#{count} commit".pluralize(count)
+
+ branch = noteable.target_branch
+ branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork?
+
+ branch_name = content_tag('code', branch)
+ content_tag('li', "#{commit_ids} - #{commits_text} from branch #{branch_name}".html_safe)
+ end
+
+ def diff_comparison_path(merge_request, project, oldrev)
+ diff_id = merge_request.merge_request_diff.id
+
+ url_helpers.diffs_project_merge_request_path(
+ project,
+ merge_request,
+ diff_id: diff_id,
+ start_sha: oldrev
+ )
+ end
+ end
+end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
new file mode 100644
index 00000000000..6fffd2ed4bf
--- /dev/null
+++ b/app/services/system_notes/issuables_service.rb
@@ -0,0 +1,312 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class IssuablesService < ::SystemNotes::BaseService
+ # Called when the assignee of a Noteable is changed or removed
+ #
+ # assignee - User being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed assignee"
+ #
+ # "assigned to @rspeicher"
+ #
+ # Returns the created Note object
+ def change_assignee(assignee)
+ body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
+ end
+
+ # Called when the assignees of an issuable is changed or removed
+ #
+ # assignees - Users being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed all assignees"
+ #
+ # "assigned to @user1 additionally to @user2"
+ #
+ # "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
+ #
+ # "assigned to @user1 and @user2"
+ #
+ # Returns the created Note object
+ def change_issuable_assignees(old_assignees)
+ unassigned_users = old_assignees - noteable.assignees
+ added_users = noteable.assignees.to_a - old_assignees
+ text_parts = []
+
+ Gitlab::I18n.with_default_locale do
+ text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
+ text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
+ end
+
+ body = text_parts.join(' and ')
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
+ end
+
+ # Called when the milestone of a Noteable is changed
+ #
+ # milestone - Milestone being assigned, or nil
+ #
+ # Example Note text:
+ #
+ # "removed milestone"
+ #
+ # "changed milestone to 7.11"
+ #
+ # Returns the created Note object
+ def change_milestone(milestone)
+ format = milestone&.group_milestone? ? :name : :iid
+ body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
+ end
+
+ # Called when the title of a Noteable is changed
+ #
+ # old_title - Previous String title
+ #
+ # Example Note text:
+ #
+ # "changed title from **Old** to **New**"
+ #
+ # Returns the created Note object
+ def change_title(old_title)
+ new_title = noteable.title.dup
+
+ old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
+
+ marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs, mode: :deletion)
+ marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs, mode: :addition)
+
+ body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ end
+
+ # Called when the description of a Noteable is changed
+ #
+ # noteable - Noteable object that responds to `description`
+ # project - Project owning noteable
+ # author - User performing the change
+ #
+ # Example Note text:
+ #
+ # "changed the description"
+ #
+ # Returns the created Note object
+ def change_description
+ body = 'changed the description'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
+ end
+
+ # Called when a Mentionable references a Noteable
+ #
+ # mentioner - Mentionable object
+ #
+ # Example Note text:
+ #
+ # "mentioned in #1"
+ #
+ # "mentioned in !2"
+ #
+ # "mentioned in 54f7727c"
+ #
+ # See cross_reference_note_content.
+ #
+ # Returns the created Note object
+ def cross_reference(mentioner)
+ return if cross_reference_disallowed?(mentioner)
+
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
+ body = cross_reference_note_content(gfm_reference)
+
+ if noteable.is_a?(ExternalIssue)
+ noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
+ else
+ create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
+ end
+ end
+
+ # Check if a cross-reference is disallowed
+ #
+ # This method prevents adding a "mentioned in !1" note on every single commit
+ # in a merge request. Additionally, it prevents the creation of references to
+ # external issues (which would fail).
+ #
+ # mentioner - Mentionable object
+ #
+ # Returns Boolean
+ def cross_reference_disallowed?(mentioner)
+ return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
+ return false unless mentioner.is_a?(MergeRequest)
+ return false unless noteable.is_a?(Commit)
+
+ mentioner.commits.include?(noteable)
+ end
+
+ # Called when the status of a Task has changed
+ #
+ # new_task - TaskList::Item object.
+ #
+ # Example Note text:
+ #
+ # "marked the task Whatever as completed."
+ #
+ # Returns the created Note object
+ def change_task_status(new_task)
+ status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
+ body = "marked the task **#{new_task.source}** as #{status_label}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
+ end
+
+ # Called when noteable has been moved to another project
+ #
+ # noteable_ref - Referenced noteable
+ # direction - symbol, :to or :from
+ #
+ # Example Note text:
+ #
+ # "moved to some_namespace/project_new#11"
+ #
+ # Returns the created Note object
+ def noteable_moved(noteable_ref, direction)
+ unless [:to, :from].include?(direction)
+ raise ArgumentError, "Invalid direction `#{direction}`"
+ end
+
+ cross_reference = noteable_ref.to_reference(project)
+ body = "moved #{direction} #{cross_reference}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
+ end
+
+ # Called when the confidentiality changes
+ #
+ # Example Note text:
+ #
+ # "made the issue confidential"
+ #
+ # Returns the created Note object
+ def change_issue_confidentiality
+ if noteable.confidential
+ body = 'made the issue confidential'
+ action = 'confidential'
+ else
+ body = 'made the issue visible to everyone'
+ action = 'visible'
+ end
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
+
+ # Called when the status of a Noteable is changed
+ #
+ # status - String status
+ # source - Mentionable performing the change, or nil
+ #
+ # Example Note text:
+ #
+ # "merged"
+ #
+ # "closed via bc17db76"
+ #
+ # Returns the created Note object
+ def change_status(status, source = nil)
+ body = status.dup
+ body << " via #{source.gfm_reference(project)}" if source
+
+ action = status == 'reopened' ? 'opened' : status
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
+
+ # Check if a cross reference to a noteable from a mentioner already exists
+ #
+ # This method is used to prevent multiple notes being created for a mention
+ # when a issue is updated, for example. The method also calls notes_for_mentioner
+ # to check if the mentioner is a commit, and return matches only on commit hash
+ # instead of project + commit, to avoid repeated mentions from forks.
+ #
+ # mentioner - Mentionable object
+ #
+ # Returns Boolean
+ def cross_reference_exists?(mentioner)
+ notes = noteable.notes.system
+ notes_for_mentioner(mentioner, noteable, notes).exists?
+ end
+
+ # Called when a Noteable has been marked as a duplicate of another Issue
+ #
+ # canonical_issue - Issue that this is a duplicate of
+ #
+ # Example Note text:
+ #
+ # "marked this issue as a duplicate of #1234"
+ #
+ # "marked this issue as a duplicate of other_project#5678"
+ #
+ # Returns the created Note object
+ def mark_duplicate_issue(canonical_issue)
+ body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
+ # Called when a Noteable has been marked as the canonical Issue of a duplicate
+ #
+ # duplicate_issue - Issue that was a duplicate of this
+ #
+ # Example Note text:
+ #
+ # "marked #1234 as a duplicate of this issue"
+ #
+ # "marked other_project#5678 as a duplicate of this issue"
+ #
+ # Returns the created Note object
+ def mark_canonical_issue_of_duplicate(duplicate_issue)
+ body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
+ def discussion_lock
+ action = noteable.discussion_locked? ? 'locked' : 'unlocked'
+ body = "#{action} this #{noteable.class.to_s.titleize.downcase}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
+
+ private
+
+ def cross_reference_note_content(gfm_reference)
+ "#{self.class.cross_reference_note_prefix}#{gfm_reference}"
+ end
+
+ def notes_for_mentioner(mentioner, noteable, notes)
+ if mentioner.is_a?(Commit)
+ text = "#{self.class.cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
+ notes.like_note_or_capitalized_note(text)
+ else
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
+ text = cross_reference_note_content(gfm_reference)
+ notes.for_note_or_capitalized_note(text)
+ end
+ end
+
+ def self.cross_reference_note_prefix
+ 'mentioned in '
+ end
+
+ def self.cross_reference?(note_text)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
+ end
+ end
+end
+
+SystemNotes::IssuablesService.prepend_if_ee('::EE::SystemNotes::IssuablesService')
diff --git a/app/services/system_notes/zoom_service.rb b/app/services/system_notes/zoom_service.rb
new file mode 100644
index 00000000000..6cd166d6cb9
--- /dev/null
+++ b/app/services/system_notes/zoom_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class ZoomService < ::SystemNotes::BaseService
+ def zoom_link_added
+ create_note(NoteSummary.new(noteable, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
+ end
+
+ def zoom_link_removed
+ create_note(NoteSummary.new(noteable, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
+ end
+ end
+end
diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb
deleted file mode 100644
index 730210c611a..00000000000
--- a/app/services/update_deployment_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateDeploymentService
- attr_reader :deployment
- attr_reader :deployable
-
- delegate :environment, to: :deployment
- delegate :variables, to: :deployable
-
- def initialize(deployment)
- @deployment = deployment
- @deployable = deployment.deployable
- end
-
- def execute
- deployment.create_ref
- deployment.invalidate_cache
-
- ActiveRecord::Base.transaction do
- environment.external_url = expanded_environment_url if
- expanded_environment_url
-
- environment.fire_state_event(action)
-
- break unless environment.save
- break if environment.stopped?
-
- deployment.tap(&:update_merge_request_metrics!)
- end
-
- deployment
- end
-
- private
-
- def environment_options
- @environment_options ||= deployable.options&.dig(:environment) || {}
- end
-
- def expanded_environment_url
- return @expanded_environment_url if defined?(@expanded_environment_url)
- return unless environment_url
-
- @expanded_environment_url =
- ExpandVariables.expand(environment_url, -> { variables })
- end
-
- def environment_url
- environment_options[:url]
- end
-
- def action
- environment_options[:action] || 'start'
- end
-end
-
-UpdateDeploymentService.prepend_if_ee('EE::UpdateDeploymentService')