diff options
Diffstat (limited to 'app/services')
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') |