diff options
Diffstat (limited to 'app/services')
132 files changed, 2168 insertions, 648 deletions
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb new file mode 100644 index 00000000000..084b103ee3b --- /dev/null +++ b/app/services/admin/propagate_integration_service.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +module Admin + class PropagateIntegrationService + BATCH_SIZE = 100 + + delegate :data_fields_present?, to: :integration + + def self.propagate(integration:, overwrite:) + new(integration, overwrite).propagate + end + + def initialize(integration, overwrite) + @integration = integration + @overwrite = overwrite + end + + def propagate + if overwrite + update_integration_for_all_projects + else + update_integration_for_inherited_projects + end + + create_integration_for_projects_without_integration + end + + private + + attr_reader :integration, :overwrite + + # rubocop: disable Cop/InBatches + # rubocop: disable CodeReuse/ActiveRecord + def update_integration_for_inherited_projects + Service.where(type: integration.type, inherit_from_id: integration.id).in_batches(of: BATCH_SIZE) do |batch| + bulk_update_from_integration(batch) + end + end + + def update_integration_for_all_projects + Service.where(type: integration.type).in_batches(of: BATCH_SIZE) do |batch| + bulk_update_from_integration(batch) + end + end + # rubocop: enable Cop/InBatches + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def bulk_update_from_integration(batch) + # Retrieving the IDs instantiates the ActiveRecord relation (batch) + # into concrete models, otherwise update_all will clear the relation. + # https://stackoverflow.com/q/34811646/462015 + batch_ids = batch.pluck(:id) + + Service.transaction do + batch.update_all(service_hash) + + if data_fields_present? + integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash) + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def create_integration_for_projects_without_integration + loop do + batch = Project.uncached { project_ids_without_integration } + + bulk_create_from_integration(batch) unless batch.empty? + + break if batch.size < BATCH_SIZE + end + end + + def bulk_create_from_integration(batch) + service_list = ServiceList.new(batch, service_hash, { 'inherit_from_id' => integration.id }).to_array + + Project.transaction do + results = bulk_insert(*service_list) + + if data_fields_present? + data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array + + bulk_insert(*data_list) + end + + run_callbacks(batch) + end + end + + def bulk_insert(klass, columns, values_array) + items_to_insert = values_array.map { |array| Hash[columns.zip(array)] } + + klass.insert_all(items_to_insert, returning: [:id]) + end + + # rubocop: disable CodeReuse/ActiveRecord + def run_callbacks(batch) + if active_external_issue_tracker? + Project.where(id: batch).update_all(has_external_issue_tracker: true) + end + + if active_external_wiki? + Project.where(id: batch).update_all(has_external_wiki: true) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def active_external_issue_tracker? + integration.issue_tracker? && !integration.default + end + + def active_external_wiki? + integration.type == 'ExternalWikiService' + end + + # rubocop: disable CodeReuse/ActiveRecord + def project_ids_without_integration + services = Service + .select('1') + .where('services.project_id = projects.id') + .where(type: integration.type) + + Project + .where('NOT EXISTS (?)', services) + .where(pending_delete: false) + .where(archived: false) + .limit(BATCH_SIZE) + .pluck(:id) + end + # rubocop: enable CodeReuse/ActiveRecord + + def service_hash + @service_hash ||= integration.to_service_hash + .tap { |json| json['inherit_from_id'] = integration.id } + end + + def data_fields_hash + @data_fields_hash ||= integration.to_data_fields_hash + end + end +end diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb new file mode 100644 index 00000000000..ffabbb37289 --- /dev/null +++ b/app/services/alert_management/alerts/update_service.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module AlertManagement + module Alerts + class UpdateService + include Gitlab::Utils::StrongMemoize + + # @param alert [AlertManagement::Alert] + # @param current_user [User] + # @param params [Hash] Attributes of the alert + def initialize(alert, current_user, params) + @alert = alert + @current_user = current_user + @params = params + end + + def execute + return error_no_permissions unless allowed? + return error_no_updates if params.empty? + + filter_assignees + old_assignees = alert.assignees.to_a + + if alert.update(params) + process_assignement(old_assignees) + + success + else + error(alert.errors.full_messages.to_sentence) + end + end + + private + + attr_reader :alert, :current_user, :params + + def allowed? + current_user&.can?(:update_alert_management_alert, alert) + end + + def assignee_todo_allowed? + assignee&.can?(:read_alert_management_alert, alert) + end + + def todo_service + strong_memoize(:todo_service) do + TodoService.new + end + end + + def success + ServiceResponse.success(payload: { alert: alert }) + end + + def error(message) + ServiceResponse.error(payload: { alert: alert }, message: message) + end + + def error_no_permissions + error(_('You have no permissions')) + end + + def error_no_updates + error(_('Please provide attributes to update')) + end + + # ----- Assignee-related behavior ------ + def filter_assignees + return if params[:assignees].nil? + + params[:assignees] = Array(assignee) + end + + def assignee + strong_memoize(:assignee) do + # Take first assignee while multiple are not currently supported + params[:assignees]&.first + end + end + + def process_assignement(old_assignees) + assign_todo + add_assignee_system_note(old_assignees) + end + + def assign_todo + # Remove check in follow-up issue https://gitlab.com/gitlab-org/gitlab/-/issues/222672 + return unless assignee_todo_allowed? + + todo_service.assign_alert(alert, current_user) + end + + def add_assignee_system_note(old_assignees) + SystemNoteService.change_issuable_assignees(alert, alert.project, current_user, old_assignees) + end + end + end +end diff --git a/app/services/alert_management/create_alert_issue_service.rb b/app/services/alert_management/create_alert_issue_service.rb index 0197f29145d..beacd240b08 100644 --- a/app/services/alert_management/create_alert_issue_service.rb +++ b/app/services/alert_management/create_alert_issue_service.rb @@ -29,8 +29,7 @@ module AlertManagement delegate :project, to: :alert def allowed? - Feature.enabled?(:alert_management_create_alert_issue, project) && - user.can?(:create_issue, project) + user.can?(:create_issue, project) end def create_issue(alert, user, alert_payload) diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb index af28f1354b3..90fcbd95e4b 100644 --- a/app/services/alert_management/process_prometheus_alert_service.rb +++ b/app/services/alert_management/process_prometheus_alert_service.rb @@ -29,6 +29,7 @@ module AlertManagement def process_firing_alert_management_alert if am_alert.present? + am_alert.register_new_event! reset_alert_management_alert_status else create_alert_management_alert @@ -47,7 +48,10 @@ module AlertManagement def create_alert_management_alert am_alert = AlertManagement::Alert.new(am_alert_params.merge(ended_at: nil)) - return if am_alert.save + if am_alert.save + am_alert.execute_services + return + end logger.warn( message: 'Unable to create AlertManagement::Alert', diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index 1de2f31f87c..c4109765a1c 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -6,19 +6,18 @@ module AutoMerge include MergeRequests::AssignsMergeParams def execute(merge_request) - assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy)) - - merge_request.auto_merge_enabled = true - merge_request.merge_user = current_user - - return :failed unless merge_request.save - - yield if block_given? + ActiveRecord::Base.transaction do + register_auto_merge_parameters!(merge_request) + yield if block_given? + end # Notify the event that auto merge is enabled or merge param is updated AutoMergeProcessWorker.perform_async(merge_request.id) strategy.to_sym + rescue => e + track_exception(e, merge_request) + :failed end def update(merge_request) @@ -30,23 +29,27 @@ module AutoMerge end def cancel(merge_request) - if clear_auto_merge_parameters(merge_request) + ActiveRecord::Base.transaction do + clear_auto_merge_parameters!(merge_request) yield if block_given? - - success - else - error("Can't cancel the automatic merge", 406) end + + success + rescue => e + track_exception(e, merge_request) + error("Can't cancel the automatic merge", 406) end def abort(merge_request, reason) - if clear_auto_merge_parameters(merge_request) + ActiveRecord::Base.transaction do + clear_auto_merge_parameters!(merge_request) yield if block_given? - - success - else - error("Can't abort the automatic merge", 406) end + + success + rescue => e + track_exception(e, merge_request) + error("Can't abort the automatic merge", 406) end def available_for?(merge_request) @@ -65,7 +68,14 @@ module AutoMerge end end - def clear_auto_merge_parameters(merge_request) + def register_auto_merge_parameters!(merge_request) + assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy)) + merge_request.auto_merge_enabled = true + merge_request.merge_user = current_user + merge_request.save! + end + + def clear_auto_merge_parameters!(merge_request) merge_request.auto_merge_enabled = false merge_request.merge_user = nil @@ -76,7 +86,11 @@ module AutoMerge 'auto_merge_strategy' ) - merge_request.save + merge_request.save! + end + + def track_exception(error, merge_request) + Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id) end end end diff --git a/app/services/award_emojis/destroy_service.rb b/app/services/award_emojis/destroy_service.rb index a61a7911a9d..cfd194262f9 100644 --- a/app/services/award_emojis/destroy_service.rb +++ b/app/services/award_emojis/destroy_service.rb @@ -13,7 +13,7 @@ module AwardEmojis return error("User has not awarded emoji of type #{name} on the awardable", status: :forbidden) end - award = awards.destroy_all.first # rubocop: disable DestroyAll + award = awards.destroy_all.first # rubocop: disable Cop/DestroyAll after_destroy(award) success(award: award) diff --git a/app/services/ci/authorize_job_artifact_service.rb b/app/services/ci/authorize_job_artifact_service.rb new file mode 100644 index 00000000000..893e92d427c --- /dev/null +++ b/app/services/ci/authorize_job_artifact_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Ci + class AuthorizeJobArtifactService + include Gitlab::Utils::StrongMemoize + + # Max size of the zipped LSIF artifact + LSIF_ARTIFACT_MAX_SIZE = 20.megabytes + LSIF_ARTIFACT_TYPE = 'lsif' + + def initialize(job, params, max_size:) + @job = job + @max_size = max_size + @size = params[:filesize] + @type = params[:artifact_type].to_s + end + + def forbidden? + lsif? && !code_navigation_enabled? + end + + def too_large? + size && max_size <= size.to_i + end + + def headers + default_headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size) + default_headers.tap do |h| + h[:ProcessLsif] = true if lsif? && code_navigation_enabled? + end + end + + private + + attr_reader :job, :size, :type + + def code_navigation_enabled? + strong_memoize(:code_navigation_enabled) do + Feature.enabled?(:code_navigation, job.project, default_enabled: true) + end + end + + def lsif? + strong_memoize(:lsif) do + type == LSIF_ARTIFACT_TYPE + end + end + + def max_size + lsif? ? LSIF_ARTIFACT_MAX_SIZE : @max_size.to_i + end + end +end diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb new file mode 100644 index 00000000000..758ba1c73bf --- /dev/null +++ b/app/services/ci/build_report_result_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Ci + class BuildReportResultService + def execute(build) + return unless Feature.enabled?(:build_report_summary, build.project) + return unless build.has_test_reports? + + build.report_results.create!( + project_id: build.project_id, + data: tests_params(build) + ) + end + + private + + def generate_test_suite_report(build) + build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + end + + def tests_params(build) + test_suite = generate_test_suite_report(build) + + { + tests: { + name: test_suite.name, + duration: test_suite.total_time, + failed: test_suite.failed_count, + errored: test_suite.error_count, + skipped: test_suite.skipped_count, + success: test_suite.success_count + } + } + end + end +end diff --git a/app/services/ci/create_cross_project_pipeline_service.rb b/app/services/ci/create_cross_project_pipeline_service.rb index a73a2e2b471..1700312b941 100644 --- a/app/services/ci/create_cross_project_pipeline_service.rb +++ b/app/services/ci/create_cross_project_pipeline_service.rb @@ -47,6 +47,7 @@ module Ci # and update the status when the downstream pipeline completes. subject.success! unless subject.dependent? else + subject.options[:downstream_errors] = pipeline.errors.full_messages subject.drop!(:downstream_pipeline_creation_failed) end end diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb new file mode 100644 index 00000000000..29d40756ab4 --- /dev/null +++ b/app/services/ci/create_web_ide_terminal_service.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +module Ci + class CreateWebIdeTerminalService < ::BaseService + include ::Gitlab::Utils::StrongMemoize + + TerminalCreationError = Class.new(StandardError) + + TERMINAL_NAME = 'terminal'.freeze + + attr_reader :terminal + + def execute + check_access! + validate_params! + load_terminal_config! + + pipeline = create_pipeline! + success(pipeline: pipeline) + rescue TerminalCreationError => e + error(e.message) + rescue ActiveRecord::RecordInvalid => e + error("Failed to persist the pipeline: #{e.message}") + end + + private + + def create_pipeline! + build_pipeline.tap do |pipeline| + pipeline.stages << terminal_stage_seed(pipeline).to_resource + pipeline.save! + + Ci::ProcessPipelineService + .new(pipeline) + .execute(nil, initial_process: true) + + pipeline_created_counter.increment(source: :webide) + end + end + + def build_pipeline + Ci::Pipeline.new( + project: project, + user: current_user, + source: :webide, + config_source: :webide_source, + ref: ref, + sha: sha, + tag: false, + before_sha: Gitlab::Git::BLANK_SHA + ) + end + + def terminal_stage_seed(pipeline) + attributes = { + name: TERMINAL_NAME, + index: 0, + builds: [terminal_build_seed] + } + + Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, []) + end + + def terminal_build_seed + terminal.merge( + name: TERMINAL_NAME, + stage: TERMINAL_NAME, + user: current_user, + scheduling_type: :stage) + end + + def load_terminal_config! + result = ::Ci::WebIdeConfigService.new(project, current_user, sha: sha).execute + raise TerminalCreationError, result[:message] if result[:status] != :success + + @terminal = result[:terminal] + raise TerminalCreationError, 'Terminal is not configured' unless terminal + end + + def validate_params! + unless sha + raise TerminalCreationError, 'Ref does not exist' + end + + unless branch_exists? + raise TerminalCreationError, 'Ref needs to be a branch' + end + end + + def check_access! + unless can?(current_user, :create_web_ide_terminal, project) + raise TerminalCreationError, 'Insufficient permissions to create a terminal' + end + + if terminal_active? + raise TerminalCreationError, 'There is already a terminal running' + end + end + + def pipeline_created_counter + @pipeline_created_counter ||= Gitlab::Metrics + .counter(:pipelines_created_total, "Counter of pipelines created") + end + + def terminal_active? + project.active_webide_pipelines(user: current_user).exists? + end + + def ref + strong_memoize(:ref) do + Gitlab::Git.ref_name(params[:ref]) + end + end + + def branch_exists? + project.repository.branch_exists?(ref) + end + + def sha + project.commit(params[:ref]).try(:id) + end + end +end diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb index 97f9918fdb7..c756e376901 100644 --- a/app/services/ci/extract_sections_from_build_trace_service.rb +++ b/app/services/ci/extract_sections_from_build_trace_service.rb @@ -5,7 +5,7 @@ module Ci def execute(build) return false unless build.trace_sections.empty? - Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) + Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) # rubocop:disable Gitlab/BulkInsert true end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 3f23e81dcdd..80ebe5f5eb6 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -11,7 +11,7 @@ module Ci def execute(trigger_build_ids = nil, initial_process: false) update_retried - if Feature.enabled?(:ci_atomic_processing, pipeline.project) + if ::Gitlab::Ci::Features.atomic_processing?(pipeline.project) Ci::PipelineProcessing::AtomicProcessingService .new(pipeline) .execute diff --git a/app/services/ci/update_ci_ref_status_service.rb b/app/services/ci/update_ci_ref_status_service.rb index 4f7ac4d11b0..22cc43232cc 100644 --- a/app/services/ci/update_ci_ref_status_service.rb +++ b/app/services/ci/update_ci_ref_status_service.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# NOTE: This class is unused and to be removed in 13.1~ module Ci class UpdateCiRefStatusService include Gitlab::OptimisticLocking diff --git a/app/services/ci/web_ide_config_service.rb b/app/services/ci/web_ide_config_service.rb new file mode 100644 index 00000000000..ade9132f419 --- /dev/null +++ b/app/services/ci/web_ide_config_service.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Ci + class WebIdeConfigService < ::BaseService + include ::Gitlab::Utils::StrongMemoize + + ValidationError = Class.new(StandardError) + + WEBIDE_CONFIG_FILE = '.gitlab/.gitlab-webide.yml'.freeze + + attr_reader :config, :config_content + + def execute + check_access! + load_config_content! + load_config! + + success(terminal: config.terminal_value) + rescue ValidationError => e + error(e.message) + end + + private + + def check_access! + unless can?(current_user, :download_code, project) + raise ValidationError, 'Insufficient permissions to read configuration' + end + end + + def load_config_content! + @config_content = webide_yaml_from_repo + + unless config_content + raise ValidationError, "Failed to load Web IDE config file '#{WEBIDE_CONFIG_FILE}' for #{params[:sha]}" + end + end + + def load_config! + @config = Gitlab::WebIde::Config.new(config_content) + + unless @config.valid? + raise ValidationError, @config.errors.first + end + rescue Gitlab::WebIde::Config::ConfigError => e + raise ValidationError, e.message + end + + def webide_yaml_from_repo + gitlab_webide_yml_for(params[:sha]) + rescue GRPC::NotFound, GRPC::Internal + nil + end + + def gitlab_webide_yml_for(sha) + project.repository.blob_data_at(sha, WEBIDE_CONFIG_FILE) + end + end +end diff --git a/app/services/clusters/applications/prometheus_config_service.rb b/app/services/clusters/applications/prometheus_config_service.rb index 34d44ab881e..50c4e26b0d0 100644 --- a/app/services/clusters/applications/prometheus_config_service.rb +++ b/app/services/clusters/applications/prometheus_config_service.rb @@ -132,19 +132,21 @@ module Clusters end def alerts(environment) - variables = Gitlab::Prometheus::QueryVariables.call(environment) alerts = Projects::Prometheus::AlertsFinder .new(environment: environment) .execute alerts.map do |alert| - substitute_query_variables(alert.to_param, variables) + hash = alert.to_param + hash['expr'] = substitute_query_variables(hash['expr'], environment) + hash end end - def substitute_query_variables(hash, variables) - hash['expr'] %= variables - hash + def substitute_query_variables(query, environment) + result = ::Prometheus::ProxyVariableSubstitutionService.new(environment, query: query).execute + + result[:params][:query] end def environments diff --git a/app/services/clusters/parse_cluster_applications_artifact_service.rb b/app/services/clusters/parse_cluster_applications_artifact_service.rb index b8e1c80cfe7..35fba5f47c7 100644 --- a/app/services/clusters/parse_cluster_applications_artifact_service.rb +++ b/app/services/clusters/parse_cluster_applications_artifact_service.rb @@ -18,13 +18,9 @@ module Clusters raise ArgumentError, 'Artifact is not cluster_applications file type' unless artifact&.cluster_applications? - unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE - return error(too_big_error_message, :bad_request) - end - - unless cluster - return error(s_('ClusterIntegration|No deployment cluster found for this job')) - end + return error(too_big_error_message, :bad_request) unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE + return error(no_deployment_message, :bad_request) unless job.deployment + return error(no_deployment_cluster_message, :bad_request) unless cluster parse!(artifact) @@ -61,7 +57,8 @@ module Clusters Clusters::Cluster.transaction do RELEASE_NAMES.each do |release_name| - application = find_or_build_application(release_name) + application_class = Clusters::Cluster::APPLICATIONS[release_name] + application = cluster.find_or_build_application(application_class) release = release_by_name[release_name] @@ -80,16 +77,18 @@ module Clusters end end - def find_or_build_application(application_name) - application_class = Clusters::Cluster::APPLICATIONS[application_name] - - cluster.find_or_build_application(application_class) - end - def too_big_error_message human_size = ActiveSupport::NumberHelper.number_to_human_size(MAX_ACCEPTABLE_ARTIFACT_SIZE) s_('ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}') % { human_size: human_size } end + + def no_deployment_message + s_('ClusterIntegration|No deployment found for this job') + end + + def no_deployment_cluster_message + s_('ClusterIntegration|No deployment cluster found for this job') + end end end diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index bd238605ac1..d80d9bebe9c 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -30,12 +30,14 @@ module Commits success(result: new_commit) rescue ChangeError => ex + Gitlab::ErrorTracking.log_exception(ex) error(ex.message, pass_back: { error_code: ex.error_code }) rescue ValidationError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError, Gitlab::Git::CommandError => ex + Gitlab::ErrorTracking.log_exception(ex) error(ex.message) end diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb index 0c5ecca3a50..4678d051d29 100644 --- a/app/services/concerns/exclusive_lease_guard.rb +++ b/app/services/concerns/exclusive_lease_guard.rb @@ -58,6 +58,6 @@ module ExclusiveLeaseGuard end def log_error(message, extra_args = {}) - Rails.logger.error(message) # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error(message) end end diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb new file mode 100644 index 00000000000..4d551430315 --- /dev/null +++ b/app/services/concerns/integrations/project_test_data.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Integrations + module ProjectTestData + private + + def push_events_data + Gitlab::DataBuilder::Push.build_sample(project, current_user) + end + + def note_events_data + note = project.notes.first + return { error: s_('TestHooks|Ensure the project has notes.') } unless note.present? + + Gitlab::DataBuilder::Note.build(note, current_user) + end + + def issues_events_data + issue = project.issues.first + return { error: s_('TestHooks|Ensure the project has issues.') } unless issue.present? + + issue.to_hook_data(current_user) + end + + def merge_requests_events_data + merge_request = project.merge_requests.first + return { error: s_('TestHooks|Ensure the project has merge requests.') } unless merge_request.present? + + merge_request.to_hook_data(current_user) + end + + def job_events_data + build = project.builds.first + return { error: s_('TestHooks|Ensure the project has CI jobs.') } unless build.present? + + Gitlab::DataBuilder::Build.build(build) + end + + def pipeline_events_data + pipeline = project.ci_pipelines.newest_first.first + return { error: s_('TestHooks|Ensure the project has CI pipelines.') } unless pipeline.present? + + Gitlab::DataBuilder::Pipeline.build(pipeline) + end + + def wiki_page_events_data + page = project.wiki.list_pages(limit: 1).first + if !project.wiki_enabled? || page.blank? + return { error: s_('TestHooks|Ensure the wiki is enabled and has pages.') } + end + + Gitlab::DataBuilder::WikiPage.build(page, current_user, 'create') + end + + def deployment_events_data + deployment = project.deployments.first + return { error: s_('TestHooks|Ensure the project has deployments.') } unless deployment.present? + + Gitlab::DataBuilder::Deployment.build(deployment) + end + end +end diff --git a/app/services/concerns/measurable.rb b/app/services/concerns/measurable.rb index 5a74f15506e..b099a58a9ae 100644 --- a/app/services/concerns/measurable.rb +++ b/app/services/concerns/measurable.rb @@ -4,8 +4,6 @@ # Example: # ``` # class DummyService -# prepend Measurable -# # def execute # # ... # end diff --git a/app/services/concerns/spam_check_methods.rb b/app/services/concerns/spam_check_methods.rb index 53e9e001463..939f8f183ab 100644 --- a/app/services/concerns/spam_check_methods.rb +++ b/app/services/concerns/spam_check_methods.rb @@ -22,15 +22,18 @@ module SpamCheckMethods # a dirty instance, which means it should be already assigned with the new # attribute values. # rubocop:disable Gitlab/ModuleWithInstanceVariables - def spam_check(spammable, user) + def spam_check(spammable, user, action:) + raise ArgumentError.new('Please provide an action, such as :create') unless action + Spam::SpamActionService.new( spammable: spammable, - request: @request + request: @request, + user: user, + context: { action: action } ).execute( api: @api, recaptcha_verified: @recaptcha_verified, - spam_log_id: @spam_log_id, - user: user) + spam_log_id: @spam_log_id) end # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/services/container_expiration_policies/update_service.rb b/app/services/container_expiration_policies/update_service.rb new file mode 100644 index 00000000000..2f34941d692 --- /dev/null +++ b/app/services/container_expiration_policies/update_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ContainerExpirationPolicies + class UpdateService < BaseContainerService + include Gitlab::Utils::StrongMemoize + + ALLOWED_ATTRIBUTES = %i[enabled cadence older_than keep_n name_regex name_regex_keep].freeze + + def execute + return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed? + + if container_expiration_policy.update(container_expiration_policy_params) + ServiceResponse.success(payload: { container_expiration_policy: container_expiration_policy }) + else + ServiceResponse.error( + message: container_expiration_policy.errors.full_messages.to_sentence || 'Bad request', + http_status: 400 + ) + end + end + + private + + def container_expiration_policy + strong_memoize(:container_expiration_policy) do + @container.container_expiration_policy || @container.build_container_expiration_policy + end + end + + def allowed? + Ability.allowed?(current_user, :destroy_container_image, @container) + end + + def container_expiration_policy_params + @params.slice(*ALLOWED_ATTRIBUTES) + end + end +end diff --git a/app/services/container_expiration_policy_service.rb b/app/services/container_expiration_policy_service.rb index 82274fd8668..80f32298323 100644 --- a/app/services/container_expiration_policy_service.rb +++ b/app/services/container_expiration_policy_service.rb @@ -1,7 +1,14 @@ # frozen_string_literal: true class ContainerExpirationPolicyService < BaseService + InvalidPolicyError = Class.new(StandardError) + def execute(container_expiration_policy) + unless container_expiration_policy.valid? + container_expiration_policy.disable! + raise InvalidPolicyError + end + container_expiration_policy.schedule_next_run! container_expiration_policy.container_repositories.find_each do |container_repository| diff --git a/app/services/design_management/delete_designs_service.rb b/app/services/design_management/delete_designs_service.rb index e69f07db5bf..5d875c630a0 100644 --- a/app/services/design_management/delete_designs_service.rb +++ b/app/services/design_management/delete_designs_service.rb @@ -15,6 +15,7 @@ module DesignManagement return error('Forbidden!') unless can_delete_designs? version = delete_designs! + EventCreateService.new.destroy_designs(designs, current_user) success(version: version) end @@ -48,7 +49,9 @@ module DesignManagement end def design_action(design) - on_success { counter.count(:delete) } + on_success do + counter.count(:delete) + end DesignManagement::DesignAction.new(design, :delete) end diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb index a09c19bc885..0446d2f1ee8 100644 --- a/app/services/design_management/save_designs_service.rb +++ b/app/services/design_management/save_designs_service.rb @@ -20,6 +20,7 @@ module DesignManagement uploaded_designs, version = upload_designs! skipped_designs = designs - uploaded_designs + create_events success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs }) rescue ::ActiveRecord::RecordInvalid => e error(e.message) @@ -47,7 +48,7 @@ module DesignManagement end def build_actions - files.zip(designs).flat_map do |(file, design)| + @actions ||= files.zip(designs).flat_map do |(file, design)| Array.wrap(build_design_action(file, design)) end end @@ -57,7 +58,9 @@ module DesignManagement return if design_unchanged?(design, content) action = new_file?(design) ? :create : :update - on_success { ::Gitlab::UsageDataCounters::DesignsCounter.count(action) } + on_success do + ::Gitlab::UsageDataCounters::DesignsCounter.count(action) + end DesignManagement::DesignAction.new(design, action, content) end @@ -67,6 +70,16 @@ module DesignManagement content == existing_blobs[design]&.data end + def create_events + by_action = @actions.group_by(&:action).transform_values { |grp| grp.map(&:design) } + + event_create_service.save_designs(current_user, **by_action) + end + + def event_create_service + @event_create_service ||= EventCreateService.new + end + def commit_message <<~MSG Updated #{files.size} #{'designs'.pluralize(files.size)} diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb index 816cd45b07a..946fb5f1372 100644 --- a/app/services/discussions/resolve_service.rb +++ b/app/services/discussions/resolve_service.rb @@ -2,25 +2,68 @@ module Discussions class ResolveService < Discussions::BaseService - def execute(one_or_more_discussions) - Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) } + include Gitlab::Utils::StrongMemoize + + def initialize(project, user = nil, params = {}) + @discussions = Array.wrap(params.fetch(:one_or_more_discussions)) + @follow_up_issue = params[:follow_up_issue] + @resolved_count = 0 + + raise ArgumentError, 'Discussions must be all for the same noteable' \ + unless noteable_is_same? + + super + end + + def execute + discussions.each(&method(:resolve_discussion)) + process_auto_merge + end + + private + + attr_accessor :discussions, :follow_up_issue + + def noteable_is_same? + return true unless discussions.size > 1 + + # Perform this check without fetching extra records + discussions.all? do |discussion| + discussion.noteable_type == first_discussion.noteable_type && + discussion.noteable_id == first_discussion.noteable_id + end end def resolve_discussion(discussion) return unless discussion.can_resolve?(current_user) discussion.resolve!(current_user) + @resolved_count += 1 - MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) if merge_request SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue end + def first_discussion + @first_discussion ||= discussions.first + end + def merge_request - params[:merge_request] + strong_memoize(:merge_request) do + first_discussion.noteable if first_discussion.for_merge_request? + end + end + + def process_auto_merge + return unless merge_request + return unless @resolved_count.positive? + return unless discussions_ready_to_merge? + + AutoMergeProcessWorker.perform_async(merge_request.id) end - def follow_up_issue - params[:follow_up_issue] + def discussions_ready_to_merge? + merge_request.auto_merge_enabled? && merge_request.mergeable_discussions_state? end end end diff --git a/app/services/draft_notes/base_service.rb b/app/services/draft_notes/base_service.rb new file mode 100644 index 00000000000..89daae0e8f4 --- /dev/null +++ b/app/services/draft_notes/base_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module DraftNotes + class BaseService < ::BaseService + attr_accessor :merge_request, :current_user, :params + + def initialize(merge_request, current_user, params = nil) + @merge_request, @current_user, @params = merge_request, current_user, params.dup + end + + private + + def draft_notes + @draft_notes ||= merge_request.draft_notes.order_id_asc.authored_by(current_user) + end + + def project + merge_request.target_project + end + end +end diff --git a/app/services/draft_notes/create_service.rb b/app/services/draft_notes/create_service.rb new file mode 100644 index 00000000000..501778b7d5f --- /dev/null +++ b/app/services/draft_notes/create_service.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module DraftNotes + class CreateService < DraftNotes::BaseService + attr_accessor :in_draft_mode, :in_reply_to_discussion_id + + def initialize(merge_request, current_user, params = nil) + @in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id) + super + end + + def execute + if in_reply_to_discussion_id.present? + unless discussion + return base_error(_('Thread to reply to cannot be found')) + end + + params[:discussion_id] = discussion.reply_id + end + + if params[:resolve_discussion] && !can_resolve_discussion? + return base_error(_('User is not allowed to resolve thread')) + end + + draft_note = DraftNote.new(params) + draft_note.merge_request = merge_request + draft_note.author = current_user + draft_note.save + + if in_reply_to_discussion_id.blank? && draft_note.diff_file&.unfolded? + merge_request.diffs.clear_cache + end + + draft_note + end + + private + + def base_error(text) + DraftNote.new.tap do |draft| + draft.errors.add(:base, text) + end + end + + def discussion + @discussion ||= merge_request.notes.find_discussion(in_reply_to_discussion_id) + end + + def can_resolve_discussion? + note = discussion&.notes&.first + return false unless note + + current_user && Ability.allowed?(current_user, :resolve_note, note) + end + end +end diff --git a/app/services/draft_notes/destroy_service.rb b/app/services/draft_notes/destroy_service.rb new file mode 100644 index 00000000000..ddca0debb03 --- /dev/null +++ b/app/services/draft_notes/destroy_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module DraftNotes + class DestroyService < DraftNotes::BaseService + # If no `draft` is given it fallsback to all + # draft notes of the given merge request and user. + def execute(draft = nil) + drafts = draft || draft_notes + + clear_highlight_diffs_cache(Array.wrap(drafts)) + + drafts.is_a?(DraftNote) ? drafts.destroy! : drafts.delete_all + end + + private + + def clear_highlight_diffs_cache(drafts) + if drafts.any? { |draft| draft.diff_file&.unfolded? } + merge_request.diffs.clear_cache + end + end + end +end diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb new file mode 100644 index 00000000000..a9a7304e5ed --- /dev/null +++ b/app/services/draft_notes/publish_service.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module DraftNotes + class PublishService < DraftNotes::BaseService + def execute(draft = nil) + return error('Not allowed to create notes') unless can?(current_user, :create_note, merge_request) + + if draft + publish_draft_note(draft) + else + publish_draft_notes + end + + success + rescue ActiveRecord::RecordInvalid => e + message = "Unable to save #{e.record.class.name}: #{e.record.errors.full_messages.join(", ")} " + error(message) + end + + private + + def publish_draft_note(draft) + create_note_from_draft(draft) + draft.delete + + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + end + + def publish_draft_notes + return if draft_notes.empty? + + review = Review.create!(author: current_user, merge_request: merge_request, project: project) + + draft_notes.map do |draft_note| + draft_note.review = review + create_note_from_draft(draft_note) + end + draft_notes.delete_all + + notification_service.async.new_review(review) + MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) + end + + def create_note_from_draft(draft) + # Make sure the diff file is unfolded in order to find the correct line + # codes. + draft.diff_file&.unfold_diff_lines(draft.original_position) + + note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute + set_discussion_resolve_status(note, draft) + + note + end + + def set_discussion_resolve_status(note, draft_note) + return unless draft_note.discussion_id.present? + + discussion = note.discussion + + if draft_note.resolve_discussion && discussion.can_resolve?(current_user) + discussion.resolve!(current_user) + else + discussion.unresolve! + end + end + end +end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 522f36cda46..89c3225dbcd 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -11,67 +11,81 @@ class EventCreateService IllegalActionError = Class.new(StandardError) def open_issue(issue, current_user) - create_record_event(issue, current_user, Event::CREATED) + create_resource_event(issue, current_user, :opened) + + create_record_event(issue, current_user, :created) end def close_issue(issue, current_user) - create_record_event(issue, current_user, Event::CLOSED) + create_resource_event(issue, current_user, :closed) + + create_record_event(issue, current_user, :closed) end def reopen_issue(issue, current_user) - create_record_event(issue, current_user, Event::REOPENED) + create_resource_event(issue, current_user, :reopened) + + create_record_event(issue, current_user, :reopened) end def open_mr(merge_request, current_user) - create_record_event(merge_request, current_user, Event::CREATED) + create_resource_event(merge_request, current_user, :opened) + + create_record_event(merge_request, current_user, :created) end def close_mr(merge_request, current_user) - create_record_event(merge_request, current_user, Event::CLOSED) + create_resource_event(merge_request, current_user, :closed) + + create_record_event(merge_request, current_user, :closed) end def reopen_mr(merge_request, current_user) - create_record_event(merge_request, current_user, Event::REOPENED) + create_resource_event(merge_request, current_user, :reopened) + + create_record_event(merge_request, current_user, :reopened) end def merge_mr(merge_request, current_user) - create_record_event(merge_request, current_user, Event::MERGED) + create_resource_event(merge_request, current_user, :merged) + + create_record_event(merge_request, current_user, :merged) end def open_milestone(milestone, current_user) - create_record_event(milestone, current_user, Event::CREATED) + create_record_event(milestone, current_user, :created) end def close_milestone(milestone, current_user) - create_record_event(milestone, current_user, Event::CLOSED) + create_record_event(milestone, current_user, :closed) end def reopen_milestone(milestone, current_user) - create_record_event(milestone, current_user, Event::REOPENED) + create_record_event(milestone, current_user, :reopened) end def destroy_milestone(milestone, current_user) - create_record_event(milestone, current_user, Event::DESTROYED) + create_record_event(milestone, current_user, :destroyed) end def leave_note(note, current_user) - create_record_event(note, current_user, Event::COMMENTED) + create_record_event(note, current_user, :commented) end def join_project(project, current_user) - create_event(project, current_user, Event::JOINED) + create_event(project, current_user, :joined) end def leave_project(project, current_user) - create_event(project, current_user, Event::LEFT) + create_event(project, current_user, :left) end def expired_leave_project(project, current_user) - create_event(project, current_user, Event::EXPIRED) + create_event(project, current_user, :expired) end def create_project(project, current_user) - create_event(project, current_user, Event::CREATED) + create_event(project, current_user, :created) end def push(project, current_user, push_data) @@ -82,11 +96,34 @@ class EventCreateService create_push_event(BulkPushEventPayloadService, project, current_user, push_data) end + def save_designs(current_user, create: [], update: []) + created = create.group_by(&:project).flat_map do |project, designs| + Feature.enabled?(:design_activity_events, project) ? designs : [] + end.to_set + updated = update.group_by(&:project).flat_map do |project, designs| + Feature.enabled?(:design_activity_events, project) ? designs : [] + end.to_set + return [] if created.empty? && updated.empty? + + records = created.zip([:created].cycle) + updated.zip([:updated].cycle) + + create_record_events(records, current_user) + end + + def destroy_designs(designs, current_user) + designs = designs.select do |design| + Feature.enabled?(:design_activity_events, design.project) + end + return [] unless designs.present? + + create_record_events(designs.zip([:destroyed].cycle), current_user) + end + # Create a new wiki page event # # @param [WikiPage::Meta] wiki_page_meta The event target # @param [User] author The event author - # @param [Integer] action One of the Event::WIKI_ACTIONS + # @param [Symbol] action One of the Event::WIKI_ACTIONS # # @return a tuple of event and either :found or :created def wiki_event(wiki_page_meta, author, action) @@ -100,7 +137,7 @@ class EventCreateService event = create_record_event(wiki_page_meta, author, action) # Ensure that the event is linked in time to the metadata, for non-deletes - unless action == Event::DESTROYED + unless event.destroyed_action? time_stamp = wiki_page_meta.updated_at event.update_columns(updated_at: time_stamp, created_at: time_stamp) end @@ -111,16 +148,41 @@ class EventCreateService private def existing_wiki_event(wiki_page_meta, action) - if action == Event::DESTROYED + if Event.actions.fetch(action) == Event.actions[:destroyed] most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first - return most_recent if most_recent.present? && most_recent.action == action + return most_recent if most_recent.present? && Event.actions[most_recent.action] == Event.actions[action] else Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first end end 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) + create_event(record.resource_parent, current_user, status, + target_id: record.id, target_type: record.class.name) + end + + # If creating several events, this method will insert them all in a single + # statement + # + # @param [[Eventable, Symbol]] a list of pairs of records and a valid status + # @param [User] the author of the event + def create_record_events(pairs, current_user) + base_attrs = { + created_at: Time.now.utc, + updated_at: Time.now.utc, + author_id: current_user.id + } + + attribute_sets = pairs.map do |record, status| + action = Event.actions[status] + raise IllegalActionError, "#{status} is not a valid status" if action.nil? + + parent_attrs(record.resource_parent) + .merge(base_attrs) + .merge(action: action, target_id: record.id, target_type: record.class.name) + end + + Event.insert_all(attribute_sets, returning: %w[id]) end def create_push_event(service_class, project, current_user, push_data) @@ -128,7 +190,7 @@ class EventCreateService # 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) + new_event = create_event(project, current_user, :pushed) service_class.new(new_event, push_data).execute @@ -146,16 +208,34 @@ class EventCreateService action: status, author_id: current_user.id ) + attributes.merge!(parent_attrs(resource_parent)) + + Event.create!(attributes) + end + def parent_attrs(resource_parent) resource_parent_attr = case resource_parent when Project - :project + :project_id when Group - :group + :group_id end - attributes[resource_parent_attr] = resource_parent if resource_parent_attr - Event.create!(attributes) + return {} unless resource_parent_attr + + { resource_parent_attr => resource_parent.id } + end + + def create_resource_event(issuable, current_user, status) + return unless state_change_tracking_enabled?(issuable) + + ResourceEvents::ChangeStateService.new(resource: issuable, user: current_user) + .execute(status) + end + + def state_change_tracking_enabled?(issuable) + issuable&.respond_to?(:resource_state_events) && + ::Feature.enabled?(:track_resource_state_change_events, issuable&.project) end end diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb index 8685850165a..14e622dd147 100644 --- a/app/services/git/wiki_push_service/change.rb +++ b/app/services/git/wiki_push_service/change.rb @@ -21,11 +21,11 @@ module Git def event_action case raw_change.operation when :added - Event::CREATED + :created when :deleted - Event::DESTROYED + :destroyed else - Event::UPDATED + :updated end end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 9437eb9eede..1bff70e6c2e 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -6,7 +6,7 @@ module Groups def async_execute job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) - Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/services/groups/group_links/create_service.rb b/app/services/groups/group_links/create_service.rb index 2ce53fcfe4a..589ac7ccde7 100644 --- a/app/services/groups/group_links/create_service.rb +++ b/app/services/groups/group_links/create_service.rb @@ -5,7 +5,7 @@ module Groups class CreateService < BaseService def execute(shared_group) unless group && shared_group && - can?(current_user, :admin_group, shared_group) && + can?(current_user, :admin_group_member, shared_group) && can?(current_user, :read_group, group) return error('Not Found', 404) end diff --git a/app/services/groups/group_links/destroy_service.rb b/app/services/groups/group_links/destroy_service.rb index 6835b6c4637..b0d496ae78c 100644 --- a/app/services/groups/group_links/destroy_service.rb +++ b/app/services/groups/group_links/destroy_service.rb @@ -3,7 +3,11 @@ module Groups module GroupLinks class DestroyService < BaseService - def execute(one_or_more_links) + def execute(one_or_more_links, skip_authorization: false) + unless skip_authorization || group && can?(current_user, :admin_group_member, group) + return error('Not Found', 404) + end + links = Array(one_or_more_links) if GroupGroupLink.delete(links) diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb index 0f2e3bb65f9..abac0ffc5d9 100644 --- a/app/services/groups/import_export/export_service.rb +++ b/app/services/groups/import_export/export_service.rb @@ -4,10 +4,11 @@ module Groups module ImportExport class ExportService def initialize(group:, user:, params: {}) - @group = group + @group = group @current_user = user - @params = params - @shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group) + @params = params + @shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group) + @logger = Gitlab::Export::Logger.build end def async_execute @@ -21,7 +22,7 @@ module Groups save! ensure - cleanup + remove_base_tmp_dir end private @@ -80,8 +81,8 @@ module Groups Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared) end - def cleanup - FileUtils.rm_rf(shared.archive_path) if shared&.archive_path + def remove_base_tmp_dir + FileUtils.rm_rf(shared.base_path) if shared&.base_path end def notify_error! @@ -91,21 +92,21 @@ module Groups end def notify_success - @shared.logger.info( - group_id: @group.id, - group_name: @group.name, - message: 'Group Import/Export: Export succeeded' + @logger.info( + message: 'Group Export succeeded', + group_id: @group.id, + group_name: @group.name ) notification_service.group_was_exported(@group, @current_user) end def notify_error - @shared.logger.error( - group_id: @group.id, + @logger.error( + message: 'Group Export failed', + group_id: @group.id, group_name: @group.name, - error: @shared.errors.join(', '), - message: 'Group Import/Export: Export failed' + errors: @shared.errors.join(', ') ) notification_service.group_was_not_exported(@group, @current_user, @shared.errors) diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index 6f692c98c38..a5c776f8fc2 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -9,6 +9,20 @@ module Groups @group = group @current_user = user @shared = Gitlab::ImportExport::Shared.new(@group) + @logger = Gitlab::Import::Logger.build + end + + def async_execute + group_import_state = GroupImportState.safe_find_or_create_by!(group: group) + jid = GroupImportWorker.perform_async(current_user.id, group.id) + + if jid.present? + group_import_state.update!(jid: jid) + else + group_import_state.fail_op('Failed to schedule import job') + + false + end end def execute @@ -21,6 +35,7 @@ module Groups end ensure + remove_base_tmp_dir remove_import_file end @@ -77,7 +92,7 @@ module Groups end def notify_success - @shared.logger.info( + @logger.info( group_id: @group.id, group_name: @group.name, message: 'Group Import/Export: Import succeeded' @@ -85,7 +100,7 @@ module Groups end def notify_error - @shared.logger.error( + @logger.error( group_id: @group.id, group_name: @group.name, message: "Group Import/Export: Errors occurred, see '#{Gitlab::ErrorTracking::Logger.file_name}' for details" @@ -97,6 +112,10 @@ module Groups raise Gitlab::ImportExport::Error.new(@shared.errors.to_sentence) end + + def remove_base_tmp_dir + FileUtils.rm_rf(@shared.base_path) + end end end end diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index fe3ab884302..fbbf4ce8baf 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -45,6 +45,7 @@ module Groups 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? + raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup? end def group_is_already_root? @@ -55,6 +56,11 @@ module Groups @new_parent_group && @new_parent_group.id == @group.parent_id end + def transfer_to_subgroup? + @new_parent_group && \ + @group.self_and_descendants.pluck_primary_key.include?(@new_parent_group.id) + end + def valid_policies? return false unless can?(current_user, :admin_group, @group) @@ -82,6 +88,7 @@ module Groups end @group.parent = @new_parent_group + @group.clear_memoization(:self_and_ancestors_ids) @group.save! end @@ -125,7 +132,8 @@ module Groups 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."), - 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.') + 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.'), + cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.') }.freeze end end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index 3c57fada677..0cf17568c78 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -10,15 +10,26 @@ module Import return error(_('This namespace has already been taken! Please choose another one.'), :unprocessable_entity) end - project = Gitlab::LegacyGithubImport::ProjectCreator - .new(repo, project_name, target_namespace, current_user, access_params, type: provider) - .execute(extra_project_attrs) + project = create_project(access_params, provider) if project.persisted? success(project) else error(project_save_error(project), :unprocessable_entity) end + rescue Octokit::Error => e + log_error(e) + end + + def create_project(access_params, provider) + Gitlab::LegacyGithubImport::ProjectCreator.new( + repo, + project_name, + target_namespace, + current_user, + access_params, + type: provider + ).execute(extra_project_attrs) end def repo @@ -44,6 +55,18 @@ module Import def authorized? can?(current_user, :create_projects, target_namespace) end + + private + + def log_error(exception) + Gitlab::Import::Logger.error( + message: 'Import failed due to a GitHub error', + status: exception.response_status, + error: exception.response_body + ) + + error(_('Import failed due to a GitHub error: %{original}') % { original: exception.response_body }, :unprocessable_entity) + end end end diff --git a/app/services/integrations/test/base_service.rb b/app/services/integrations/test/base_service.rb new file mode 100644 index 00000000000..a8a027092d5 --- /dev/null +++ b/app/services/integrations/test/base_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Integrations + module Test + class BaseService + include BaseServiceUtility + + attr_accessor :integration, :current_user, :event + + # @param integration [Service] The external service that will be called + # @param current_user [User] The user calling the service + # @param event [String/nil] The event that triggered this + def initialize(integration, current_user, event = nil) + @integration = integration + @current_user = current_user + @event = event + end + + def execute + if event && (integration.supported_events.exclude?(event) || data.blank?) + return error('Testing not available for this event') + end + + return error(data[:error]) if data[:error].present? + + integration.test(data) + end + + private + + def data + raise NotImplementedError + end + end + end +end diff --git a/app/services/integrations/test/project_service.rb b/app/services/integrations/test/project_service.rb new file mode 100644 index 00000000000..941d70c2cc4 --- /dev/null +++ b/app/services/integrations/test/project_service.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Integrations + module Test + class ProjectService < Integrations::Test::BaseService + include Integrations::ProjectTestData + include Gitlab::Utils::StrongMemoize + + def project + strong_memoize(:project) do + integration.project + end + end + + private + + def data + strong_memoize(:data) do + next pipeline_events_data if integration.is_a?(::PipelinesEmailService) + + case event + when 'push', 'tag_push' + push_events_data + when 'note', 'confidential_note' + note_events_data + when 'issue', 'confidential_issue' + issues_events_data + when 'merge_request' + merge_requests_events_data + when 'job' + job_events_data + when 'pipeline' + pipeline_events_data + when 'wiki_page' + wiki_page_events_data + when 'deployment' + deployment_events_data + else + push_events_data + end + end + end + end + end +end + +Integrations::Test::ProjectService.prepend_if_ee('::EE::Integrations::Test::ProjectService') diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 2cd0e1e992d..2902385da4a 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -17,9 +17,8 @@ module Issuable ids = params.delete(:issuable_ids).split(",") items = find_issuables(parent, model_class, ids) - permitted_attrs(type).each do |key| - params.delete(key) unless params[key].present? - end + params.slice!(*permitted_attrs(type)) + params.delete_if { |k, v| v.blank? } if params[:assignee_ids] == [IssuableFinder::Params::NONE.to_s] params[:assignee_ids] = [] @@ -40,9 +39,13 @@ module Issuable private def permitted_attrs(type) - attrs = %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event) + attrs = %i(state_event milestone_id add_label_ids remove_label_ids subscription_event) + + issuable_specific_attrs(type, attrs) + end - if type == 'issue' + def issuable_specific_attrs(type, attrs) + if type == 'issue' || type == 'merge_request' attrs.push(:assignee_ids) else attrs.push(:assignee_id) diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb index a78e191c85f..b185ab592ff 100644 --- a/app/services/issuable/clone/attributes_rewriter.rb +++ b/app/services/issuable/clone/attributes_rewriter.rb @@ -105,7 +105,7 @@ module Issuable yield(event) end.compact - Gitlab::Database.bulk_insert(table_name, events) + Gitlab::Database.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 18062bd60da..38b10996f44 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -129,15 +129,11 @@ class IssuableBaseService < BaseService add_label_ids = attributes.delete(:add_label_ids) remove_label_ids = attributes.delete(:remove_label_ids) - new_label_ids = existing_label_ids || label_ids || [] + new_label_ids = label_ids || existing_label_ids || [] new_label_ids |= extra_label_ids - if add_label_ids.blank? && remove_label_ids.blank? - new_label_ids = label_ids if label_ids - else - new_label_ids |= add_label_ids if add_label_ids - new_label_ids -= remove_label_ids if remove_label_ids - end + new_label_ids |= add_label_ids if add_label_ids + new_label_ids -= remove_label_ids if remove_label_ids new_label_ids.uniq end @@ -350,7 +346,7 @@ class IssuableBaseService < BaseService todo_service.mark_todo(issuable, current_user) when 'done' todo = TodosFinder.new(current_user).find_by(target: issuable) - todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo + todo_service.resolve_todo(todo, current_user) if todo end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 7869509aa9c..c0194f5b847 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -15,7 +15,7 @@ module Issues end def before_create(issue) - spam_check(issue, current_user) + spam_check(issue, current_user, action: :create) issue.move_to_end # current_user (defined in BaseService) is not available within run_after_commit block @@ -38,9 +38,8 @@ module Issues return if discussions_to_resolve.empty? Discussions::ResolveService.new(project, current_user, - merge_request: merge_request_to_resolve_discussions_of, - follow_up_issue: issue) - .execute(discussions_to_resolve) + one_or_more_discussions: discussions_to_resolve, + follow_up_issue: issue).execute end private diff --git a/app/services/issues/import_csv_service.rb b/app/services/issues/import_csv_service.rb index c01db5fcfe6..60790ba3547 100644 --- a/app/services/issues/import_csv_service.rb +++ b/app/services/issues/import_csv_service.rb @@ -46,7 +46,7 @@ module Issues end def email_results_to_user - Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_now + Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_later end def detect_col_sep(header) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index ee1a22634af..8d22f0edcdd 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -18,7 +18,7 @@ module Issues end def before_update(issue, skip_spam_check: false) - spam_check(issue, current_user) unless skip_spam_check + spam_check(issue, current_user, action: :update) unless skip_spam_check end def after_update(issue) @@ -32,7 +32,7 @@ module Issues old_assignees = old_associations.fetch(:assignees, []) if has_changes?(issue, old_labels: old_labels, old_assignees: old_assignees) - todo_service.mark_pending_todos_as_done(issue, current_user) + todo_service.resolve_todos_for_target(issue, current_user) end if issue.previous_changes.include?('title') || @@ -68,7 +68,7 @@ module Issues end def handle_task_changes(issuable) - todo_service.mark_pending_todos_as_done(issuable, current_user) + todo_service.resolve_todos_for_target(issuable, current_user) todo_service.update_issue(issuable, current_user) end diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb new file mode 100644 index 00000000000..7521c7610cb --- /dev/null +++ b/app/services/jira/requests/base.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Jira + module Requests + class Base + include ProjectServicesLoggable + + PER_PAGE = 50 + + attr_reader :jira_service, :project, :limit, :start_at, :query + + def initialize(jira_service, limit: PER_PAGE, start_at: 0, query: nil) + @project = jira_service&.project + @jira_service = jira_service + + @limit = limit + @start_at = start_at + @query = query + end + + def execute + return ServiceResponse.error(message: _('Jira service not configured.')) unless jira_service&.active? + return ServiceResponse.success(payload: empty_payload) if limit.to_i <= 0 + + request + end + + private + + def client + @client ||= jira_service.client + end + + def request + response = client.get(url) + build_service_response(response) + rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error + error_message = "Jira request error: #{error.message}" + log_error("Error sending message", client_url: client.options[:site], error: error_message) + ServiceResponse.error(message: error_message) + end + + def url + raise NotImplementedError + end + + def build_service_response(response) + raise NotImplementedError + end + end + end +end diff --git a/app/services/jira/requests/projects.rb b/app/services/jira/requests/projects.rb new file mode 100644 index 00000000000..da464503211 --- /dev/null +++ b/app/services/jira/requests/projects.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Jira + module Requests + class Projects < Base + extend ::Gitlab::Utils::Override + + private + + override :url + def url + '/rest/api/2/project/search?query=%{query}&maxResults=%{limit}&startAt=%{start_at}' % + { query: CGI.escape(query.to_s), limit: limit.to_i, start_at: start_at.to_i } + end + + override :build_service_response + def build_service_response(response) + return ServiceResponse.success(payload: empty_payload) unless response['values'].present? + + ServiceResponse.success(payload: { projects: map_projects(response), is_last: response['isLast'] }) + end + + def map_projects(response) + response['values'].map { |v| JIRA::Resource::Project.build(client, v) } + end + + def empty_payload + { projects: [], is_last: true } + end + end + end +end diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb index 59fd463022f..a06cc6df719 100644 --- a/app/services/jira_import/start_import_service.rb +++ b/app/services/jira_import/start_import_service.rb @@ -28,8 +28,8 @@ module JiraImport rescue => ex # in case project.save! raises an erorr Gitlab::ErrorTracking.track_exception(ex, project_id: project.id) + jira_import&.do_fail!(error_message: ex.message) build_error_response(ex.message) - jira_import.do_fail! end def build_jira_import @@ -62,7 +62,7 @@ module JiraImport end def validate - project.validate_jira_import_settings!(user: user) + Gitlab::JiraImport.validate_project_settings!(project, user: user) return build_error_response(_('Unable to find Jira project to import data from.')) if jira_project_key.blank? return build_error_response(_('Jira import is already running.')) if import_in_progress? diff --git a/app/services/jira_import/users_importer.rb b/app/services/jira_import/users_importer.rb new file mode 100644 index 00000000000..579d3675073 --- /dev/null +++ b/app/services/jira_import/users_importer.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module JiraImport + class UsersImporter + attr_reader :user, :project, :start_at, :result + + MAX_USERS = 50 + + def initialize(user, project, start_at) + @project = project + @start_at = start_at + @user = user + end + + def execute + Gitlab::JiraImport.validate_project_settings!(project, user: user) + + return ServiceResponse.success(payload: nil) if users.blank? + + result = UsersMapper.new(project, users).execute + ServiceResponse.success(payload: result) + rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error + Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url) + ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}") + rescue Projects::ImportService::Error => error + ServiceResponse.error(message: error.message) + end + + private + + def users + @users ||= client.get(url) + end + + def url + "/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}" + end + + def client + @client ||= project.jira_service.client + end + end +end diff --git a/app/services/jira_import/users_mapper.rb b/app/services/jira_import/users_mapper.rb new file mode 100644 index 00000000000..31a3f721556 --- /dev/null +++ b/app/services/jira_import/users_mapper.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module JiraImport + class UsersMapper + attr_reader :project, :jira_users + + def initialize(project, jira_users) + @project = project + @jira_users = jira_users + end + + def execute + jira_users.to_a.map do |jira_user| + { + jira_account_id: jira_user['accountId'], + jira_display_name: jira_user['displayName'], + jira_email: jira_user['emailAddress'], + gitlab_id: match_user(jira_user) + } + end + end + + private + + # TODO: Matching user by email and displayName will be done as the part + # of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023 + def match_user(jira_user) + nil + end + end +end diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb index 32c4ab645df..c256de7b35d 100644 --- a/app/services/keys/create_service.rb +++ b/app/services/keys/create_service.rb @@ -2,6 +2,14 @@ module Keys class CreateService < ::Keys::BaseService + attr_accessor :current_user + + def initialize(current_user, params = {}) + @current_user, @params = current_user, params + @ip_address = @params.delete(:ip_address) + @user = params.delete(:user) || current_user + end + def execute key = user.keys.create(params) notification_service.new_key(key) if key.persisted? diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb index 8886e58d6ef..979964e09fd 100644 --- a/app/services/labels/available_labels_service.rb +++ b/app/services/labels/available_labels_service.rb @@ -30,11 +30,13 @@ module Labels end def filter_labels_ids_in_param(key) - return [] if params[key].to_a.empty? + ids = params[key].to_a + return [] if ids.empty? # rubocop:disable CodeReuse/ActiveRecord - available_labels.by_ids(params[key]).pluck(:id) + existing_ids = available_labels.by_ids(ids).pluck(:id) # rubocop:enable CodeReuse/ActiveRecord + ids.map(&:to_i) & existing_ids end private diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb index c032985be42..a5b30e29e55 100644 --- a/app/services/labels/create_service.rb +++ b/app/services/labels/create_service.rb @@ -20,7 +20,7 @@ module Labels label.save label else - Rails.logger.warn("target_params should contain :project or :group or :template, actual value: #{target_params}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("target_params should contain :project or :group or :template, actual value: #{target_params}") end end end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index cc91fd4b4d2..9ed10f6a11b 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -90,7 +90,7 @@ module Labels # rubocop: disable CodeReuse/ActiveRecord def destroy_project_labels(label_ids) - Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll + Label.where(id: label_ids).destroy_all # rubocop: disable Cop/DestroyAll end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 31097b9151a..8d57a76f7d0 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -121,12 +121,12 @@ module MergeRequests end def handle_merge_error(log_message:, save_message_on_model: false) - Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") @merge_request.update(merge_error: log_message) if save_message_on_model end def log_info(message) - @logger ||= Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger ||= Gitlab::AppLogger @logger.info("#{merge_request_info} - #{message}") end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 2d33e87bf4b..561695baeab 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -27,7 +27,7 @@ module MergeRequests old_assignees = old_associations.fetch(:assignees, []) if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees) - todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.resolve_todos_for_target(merge_request, current_user) end if merge_request.previous_changes.include?('title') || @@ -73,7 +73,7 @@ module MergeRequests end def handle_task_changes(merge_request) - todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.resolve_todos_for_target(merge_request, current_user) todo_service.update_merge_request(merge_request, current_user) end diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb index 514793694ba..c2a0f22e73e 100644 --- a/app/services/metrics/dashboard/base_service.rb +++ b/app/services/metrics/dashboard/base_service.rb @@ -13,7 +13,8 @@ module Metrics STAGES::EndpointInserter, STAGES::PanelIdsInserter, STAGES::Sorter, - STAGES::AlertsInserter + STAGES::AlertsInserter, + STAGES::UrlValidator ].freeze def get_dashboard diff --git a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb index d97668d1c7c..8599c23c206 100644 --- a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb +++ b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb @@ -6,7 +6,7 @@ module Metrics module Dashboard class SelfMonitoringDashboardService < ::Metrics::Dashboard::PredefinedDashboardService DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml' - DASHBOARD_NAME = 'Default' + DASHBOARD_NAME = N_('Default dashboard') SEQUENCE = [ STAGES::CustomMetricsInserter, @@ -23,7 +23,7 @@ module Metrics def all_dashboard_paths(_project) [{ path: DASHBOARD_PATH, - display_name: DASHBOARD_NAME, + display_name: _(DASHBOARD_NAME), default: true, system_dashboard: false }] diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb index ed4b78ba159..db5599b4def 100644 --- a/app/services/metrics/dashboard/system_dashboard_service.rb +++ b/app/services/metrics/dashboard/system_dashboard_service.rb @@ -6,7 +6,7 @@ module Metrics module Dashboard class SystemDashboardService < ::Metrics::Dashboard::PredefinedDashboardService DASHBOARD_PATH = 'config/prometheus/common_metrics.yml' - DASHBOARD_NAME = 'Default' + DASHBOARD_NAME = N_('Default dashboard') SEQUENCE = [ STAGES::CommonMetricsInserter, @@ -22,7 +22,7 @@ module Metrics def all_dashboard_paths(_project) [{ path: DASHBOARD_PATH, - display_name: DASHBOARD_NAME, + display_name: _(DASHBOARD_NAME), default: true, system_dashboard: true }] diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 80e6456f729..2431318cbb2 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -76,7 +76,7 @@ module Milestones # rubocop: disable CodeReuse/ActiveRecord def destroy_old_milestones(milestone) - Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable DestroyAll + Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable Cop/DestroyAll end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/namespaces/check_storage_size_service.rb b/app/services/namespaces/check_storage_size_service.rb index b3cf17681ee..57d2645a0c8 100644 --- a/app/services/namespaces/check_storage_size_service.rb +++ b/app/services/namespaces/check_storage_size_service.rb @@ -41,7 +41,8 @@ module Namespaces { explanation_message: explanation_message, usage_message: usage_message, - alert_level: alert_level + alert_level: alert_level, + root_namespace: root_namespace } end @@ -50,7 +51,7 @@ module Namespaces end def usage_message - s_("You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})" % current_usage_params) + s_("You reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})" % current_usage_params) end def alert_level diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 6c1f52ec866..935dbfb72dd 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -88,9 +88,11 @@ module Notes end end - # EE::Notes::CreateService would override this method def quick_action_options - { merge_request_diff_head_sha: params[:merge_request_diff_head_sha] } + { + merge_request_diff_head_sha: params[:merge_request_diff_head_sha], + review_id: params[:review_id] + } end def tracking_data_for(note) @@ -103,5 +105,3 @@ module Notes end end end - -Notes::CreateService.prepend_if_ee('EE::Notes::CreateService') diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index bc86118a150..0e455c641ce 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -36,7 +36,7 @@ module Notes return unless @note.project note_data = hook_data - hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks + hooks_scope = @note.confidential?(include_noteable: true) ? :confidential_note_hooks : :note_hooks @note.project.execute_hooks(note_data, hooks_scope) @note.project.execute_services(note_data, hooks_scope) diff --git a/app/services/notification_recipients/build_service.rb b/app/services/notification_recipients/build_service.rb index df807f11e1b..0fe0d26d7b2 100644 --- a/app/services/notification_recipients/build_service.rb +++ b/app/services/notification_recipients/build_service.rb @@ -32,7 +32,9 @@ module NotificationRecipients def self.build_new_release_recipients(*args) ::NotificationRecipients::Builder::NewRelease.new(*args).notification_recipients end + + def self.build_new_review_recipients(*args) + ::NotificationRecipients::Builder::NewReview.new(*args).notification_recipients + end end end - -NotificationRecipients::BuildService.prepend_if_ee('EE::NotificationRecipients::BuildService') diff --git a/app/services/notification_recipients/builder/new_review.rb b/app/services/notification_recipients/builder/new_review.rb new file mode 100644 index 00000000000..3b1296f6967 --- /dev/null +++ b/app/services/notification_recipients/builder/new_review.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module NotificationRecipients + module Builder + class NewReview < Base + attr_reader :review + def initialize(review) + @review = review + end + + def target + review.merge_request + end + + def project + review.project + end + + def group + project.group + end + + def build! + add_participants(review.author) + add_mentions(review.author, target: review) + add_project_watchers + add_custom_notifications + add_subscribed_users + end + + # A new review is a batch of new notes + # therefore new_note subscribers should also + # receive incoming new reviews + def custom_action + :new_note + end + + def acting_user + review.author + end + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4c1db03fab8..73e60ac8420 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -68,10 +68,10 @@ class NotificationService # Notify a user when a previously unknown IP or device is used to # sign in to their account - def unknown_sign_in(user, ip) + def unknown_sign_in(user, ip, time) return unless user.can?(:receive_notifications) - mailer.unknown_sign_in_email(user, ip).deliver_later + mailer.unknown_sign_in_email(user, ip, time).deliver_later end # When create an issue we should send an email to: @@ -447,14 +447,14 @@ class NotificationService # from the PipelinesEmailService integration. return if pipeline.project.emails_disabled? - ref_status ||= pipeline.status - email_template = "pipeline_#{ref_status}_email" + status = pipeline_notification_status(ref_status, pipeline) + email_template = "pipeline_#{status}_email" return unless mailer.respond_to?(email_template) recipients ||= notifiable_users( [pipeline.user], :watch, - custom_action: :"#{ref_status}_pipeline", + custom_action: :"#{status}_pipeline", target: pipeline ).map do |user| user.notification_email_for(pipeline.project.group) @@ -557,6 +557,15 @@ class NotificationService mailer.group_was_not_exported_email(current_user, group, errors).deliver_later end + # Notify users on new review in system + def new_review(review) + recipients = NotificationRecipients::BuildService.build_new_review_recipients(review) + + recipients.each do |recipient| + mailer.new_review_email(recipient.user.id, review.id).deliver_later + end + end + protected def new_resource_email(target, method) @@ -652,6 +661,16 @@ class NotificationService private + def pipeline_notification_status(ref_status, pipeline) + if Ci::Ref.failing_state?(ref_status) + 'failed' + elsif ref_status + ref_status + else + pipeline.status + end + end + def owners_and_maintainers_without_invites(project) recipients = project.members.active_without_invites_and_requests.owners_and_maintainers diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb index d4de6bb750d..7408943e78c 100644 --- a/app/services/pages/delete_service.rb +++ b/app/services/pages/delete_service.rb @@ -4,7 +4,7 @@ module Pages class DeleteService < BaseService def execute project.remove_pages - project.pages_domains.destroy_all # rubocop: disable DestroyAll + project.pages_domains.destroy_all # rubocop: disable Cop/DestroyAll end end end diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb index ee2dde8aa7f..fad2290a47b 100644 --- a/app/services/projects/after_import_service.rb +++ b/app/services/projects/after_import_service.rb @@ -22,8 +22,12 @@ module Projects # 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}") + Gitlab::Import::Logger.info( + message: 'Project housekeeping failed', + project_full_path: @project.full_path, + project_id: @project.id, + error: e.message + ) end private diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb index 76c89e85f17..86c408aeec8 100644 --- a/app/services/projects/alerting/notify_service.rb +++ b/app/services/projects/alerting/notify_service.rb @@ -10,7 +10,7 @@ module Projects return forbidden unless alerts_service_activated? return unauthorized unless valid_token?(token) - alert = create_alert + alert = process_alert return bad_request unless alert.persisted? process_incident_issues(alert) if process_issues? @@ -26,11 +26,36 @@ module Projects delegate :alerts_service, :alerts_service_activated?, to: :project def am_alert_params - Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h) + strong_memoize(:am_alert_params) do + Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h) + end + end + + def process_alert + existing_alert = find_alert_by_fingerprint(am_alert_params[:fingerprint]) + + if existing_alert + process_existing_alert(existing_alert) + else + create_alert + end + end + + def process_existing_alert(alert) + alert.register_new_event! end def create_alert - AlertManagement::Alert.create(am_alert_params) + alert = AlertManagement::Alert.create(am_alert_params) + alert.execute_services if alert.persisted? + + alert + end + + def find_alert_by_fingerprint(fingerprint) + return unless fingerprint + + AlertManagement::Alert.for_fingerprint(project, fingerprint).first end def send_email? @@ -38,6 +63,8 @@ module Projects end def process_incident_issues(alert) + return if alert.issue + IncidentManagement::ProcessAlertWorker .perform_async(project.id, parsed_payload, alert.id) end diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index b53a9c1561e..c5809c11ea9 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -6,6 +6,7 @@ module Projects def execute(container_repository) return error('feature disabled') unless can_use? return error('access denied') unless can_destroy? + return error('invalid regex') unless valid_regex? tags = container_repository.tags tags = without_latest(tags) @@ -76,6 +77,17 @@ module Projects def can_use? Feature.enabled?(:container_registry_cleanup, project, default_enabled: true) end + + def valid_regex? + %w(name_regex_delete name_regex name_regex_keep).each do |param_name| + regex = params[param_name] + Gitlab::UntrustedRegexp.new(regex) unless regex.blank? + end + true + rescue RegexpError => e + Gitlab::ErrorTracking.log_exception(e, project_id: project.id) + false + end end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 3233d1799b8..bffd443c49f 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -150,7 +150,7 @@ module Projects if @project.save unless @project.gitlab_project_import? - create_services_from_active_templates(@project) + create_services_from_active_instances_or_templates(@project) @project.create_labels end @@ -166,7 +166,7 @@ module Projects log_message = message.dup log_message << " Project ID: #{@project.id}" if @project&.id - Rails.logger.error(log_message) # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error(log_message) if @project && @project.persisted? && @project.import_state @project.import_state.mark_as_failed(message) @@ -175,15 +175,6 @@ module Projects @project end - # rubocop: disable CodeReuse/ActiveRecord - def create_services_from_active_templates(project) - Service.where(template: true, active: true).each do |template| - service = Service.build_from_template(project.id, template) - service.save! - end - end - # rubocop: enable CodeReuse/ActiveRecord - def create_prometheus_service service = @project.find_or_initialize_service(::PrometheusService.to_param) @@ -225,6 +216,15 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord + def create_services_from_active_instances_or_templates(project) + Service.active.where(instance: true).or(Service.active.where(template: true)).group_by(&:type).each do |type, records| + service = records.find(&:instance?) || records.find(&:template?) + Service.build_from_integration(project.id, service).save! + end + end + # rubocop: enable CodeReuse/ActiveRecord + def project_namespace @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace end @@ -249,9 +249,7 @@ module Projects end end -# rubocop: disable Cop/InjectEnterpriseEditionModule Projects::CreateService.prepend_if_ee('EE::Projects::CreateService') -# rubocop: enable Cop/InjectEnterpriseEditionModule # Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::CreateService as well Projects::CreateService.prepend(Measurable) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index fd1366d2c4a..2e949f2fc55 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -64,7 +64,7 @@ module Projects end def remove_snippets - response = Snippets::BulkDestroyService.new(current_user, project.snippets).execute + response = ::Snippets::BulkDestroyService.new(current_user, project.snippets).execute response.success? end diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb index 942cd8162e4..c57773c3302 100644 --- a/app/services/projects/detect_repository_languages_service.rb +++ b/app/services/projects/detect_repository_languages_service.rb @@ -21,7 +21,7 @@ module Projects .update_all(share: update[:share]) end - Gitlab::Database.bulk_insert( + Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert RepositoryLanguage.table_name, detection.insertions(matching_programming_languages) ) diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb index 241948b335b..2ba3cd6694f 100644 --- a/app/services/projects/group_links/create_service.rb +++ b/app/services/projects/group_links/create_service.rb @@ -13,6 +13,7 @@ module Projects ) if link.save + group.refresh_members_authorized_projects success(link: link) else error(link.errors.full_messages.to_sentence, 409) diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb index ea7d05551fd..229191e41f6 100644 --- a/app/services/projects/group_links/destroy_service.rb +++ b/app/services/projects/group_links/destroy_service.rb @@ -12,7 +12,9 @@ module Projects TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id) end - group_link.destroy + group_link.destroy.tap do |link| + link.group.refresh_members_authorized_projects + end end end end diff --git a/app/services/projects/group_links/update_service.rb b/app/services/projects/group_links/update_service.rb new file mode 100644 index 00000000000..7de4b7a211d --- /dev/null +++ b/app/services/projects/group_links/update_service.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Projects + module GroupLinks + class UpdateService < BaseService + def initialize(group_link, user = nil) + super(group_link.project, user) + + @group_link = group_link + end + + def execute(group_link_params) + group_link.update!(group_link_params) + + if requires_authorization_refresh?(group_link_params) + group_link.group.refresh_members_authorized_projects + end + end + + private + + attr_reader :group_link + + def requires_authorization_refresh?(params) + params.include?(:group_access) + end + end + end +end diff --git a/app/services/projects/hashed_storage/base_attachment_service.rb b/app/services/projects/hashed_storage/base_attachment_service.rb index a2a7895ba17..d61a2af6c1c 100644 --- a/app/services/projects/hashed_storage/base_attachment_service.rb +++ b/app/services/projects/hashed_storage/base_attachment_service.rb @@ -19,7 +19,7 @@ module Projects def initialize(project:, old_disk_path:, logger: nil) @project = project @old_disk_path = old_disk_path - @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger = logger || Gitlab::AppLogger end # Return whether this operation was skipped or not diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 86cb4f35206..031b99753c3 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -9,6 +9,7 @@ module Projects super @shared = project.import_export_shared + @logger = Gitlab::Export::Logger.build end def execute(after_export_strategy = nil) @@ -115,11 +116,20 @@ module Projects end def notify_success - Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported") # rubocop:disable Gitlab/RailsLogger + @logger.info( + message: 'Project successfully exported', + project_name: project.name, + project_id: project.id + ) end def notify_error - Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{shared.errors.join(', ')}") # rubocop:disable Gitlab/RailsLogger + @logger.error( + message: 'Project export error', + export_errors: shared.errors.join(', '), + project_name: project.name, + project_id: project.id + ) notification_service.project_not_exported(project, current_user, shared.errors) end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 449c4c3de6b..b4abb5b6df7 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -149,9 +149,7 @@ module Projects end end -# rubocop: disable Cop/InjectEnterpriseEditionModule Projects::ImportService.prepend_if_ee('EE::Projects::ImportService') -# rubocop: enable Cop/InjectEnterpriseEditionModule # Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::ImportService as well Projects::ImportService.prepend(Measurable) diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb index 39cd553261f..e86106f0a09 100644 --- a/app/services/projects/lfs_pointers/lfs_link_service.rb +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -38,7 +38,7 @@ module Projects rows = existent_lfs_objects .not_linked_to_project(project) .map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } } - Gitlab::Database.bulk_insert(:lfs_objects_projects, rows) + Gitlab::Database.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert iterations += 1 linked_existing_objects += existent_lfs_objects.map(&:oid) diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb deleted file mode 100644 index 5e7055b3309..00000000000 --- a/app/services/projects/lsif_data_service.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -module Projects - class LsifDataService - attr_reader :file, :project, :commit_id, :docs, - :doc_ranges, :ranges, :def_refs, :hover_refs - - CACHE_EXPIRE_IN = 1.hour - - def initialize(file, project, commit_id) - @file = file - @project = project - @commit_id = commit_id - - fetch_data! - end - - def execute(path) - doc_id = find_doc_id(docs, path) - dir_absolute_path = docs[doc_id]&.delete_suffix(path) - - doc_ranges[doc_id]&.map do |range_id| - location, ref_id = ranges[range_id].values_at('loc', 'ref_id') - line_data, column_data = location - - { - start_line: line_data.first, - end_line: line_data.last, - start_char: column_data.first, - end_char: column_data.last, - definition_url: definition_url_for(def_refs[ref_id], dir_absolute_path), - hover: highlighted_hover(hover_refs[ref_id]) - } - end - end - - private - - def fetch_data - Rails.cache.fetch("project:#{project.id}:lsif:#{commit_id}", expires_in: CACHE_EXPIRE_IN) do - data = nil - - file.open do |stream| - Zlib::GzipReader.wrap(stream) do |gz_stream| - data = Gitlab::Json.parse(gz_stream.read) - end - end - - data - end - end - - def fetch_data! - data = fetch_data - - @docs = data['docs'] - @doc_ranges = data['doc_ranges'] - @ranges = data['ranges'] - @def_refs = data['def_refs'] - @hover_refs = data['hover_refs'] - end - - def find_doc_id(docs, path) - docs.reduce(nil) do |doc_id, (id, doc_path)| - next doc_id unless doc_path =~ /#{path}$/ - - if doc_id.nil? || docs[doc_id].size > doc_path.size - doc_id = id - end - - doc_id - end - end - - def definition_url_for(ref_id, dir_absolute_path) - return unless range = ranges[ref_id] - - def_doc_id, location = range.values_at('doc_id', 'loc') - localized_doc_url = docs[def_doc_id].delete_prefix(dir_absolute_path) - - # location is stored as [[start_line, end_line], [start_char, end_char]] - start_line = location.first.first - - line_anchor = "L#{start_line + 1}" - definition_ref_path = [commit_id, localized_doc_url].join('/') - - Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor) - end - - def highlighted_hover(hovers) - hovers&.map do |hover| - # Documentation for a method which is added as comments on top of the method - # is stored as a raw string value in LSIF file - next { value: hover } unless hover.is_a?(Hash) - - value = Gitlab::Highlight.highlight(nil, hover['value'], language: hover['language']) - { language: hover['language'], value: value } - end - end - end -end diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb index 01419563538..51d84af249e 100644 --- a/app/services/projects/move_deploy_keys_projects_service.rb +++ b/app/services/projects/move_deploy_keys_projects_service.rb @@ -28,7 +28,7 @@ module Projects # rubocop: enable CodeReuse/ActiveRecord def remove_remaining_deploy_keys_projects - source_project.deploy_keys_projects.destroy_all # rubocop: disable DestroyAll + source_project.deploy_keys_projects.destroy_all # rubocop: disable Cop/DestroyAll end end end diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb index 8cc420d7ba7..57a8d3d69c6 100644 --- a/app/services/projects/move_lfs_objects_projects_service.rb +++ b/app/services/projects/move_lfs_objects_projects_service.rb @@ -20,7 +20,7 @@ module Projects end def remove_remaining_lfs_objects_project - source_project.lfs_objects_projects.destroy_all # rubocop: disable DestroyAll + source_project.lfs_objects_projects.destroy_all # rubocop: disable Cop/DestroyAll end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb index 65a888fe26b..efe06f158cc 100644 --- a/app/services/projects/move_notification_settings_service.rb +++ b/app/services/projects/move_notification_settings_service.rb @@ -21,7 +21,7 @@ module Projects # Remove remaining notification settings from source_project def remove_remaining_notification_settings - source_project.notification_settings.destroy_all # rubocop: disable DestroyAll + source_project.notification_settings.destroy_all # rubocop: disable Cop/DestroyAll end # Get users of current notification_settings diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index d1aa9af2bcb..349953ff973 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -25,7 +25,7 @@ module Projects # Remove remaining project group links from source_project def remove_remaining_project_group_links - source_project.reset.project_group_links.destroy_all # rubocop: disable DestroyAll + source_project.reset.project_group_links.destroy_all # rubocop: disable Cop/DestroyAll end def group_links_in_target_project diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index de4e7e5a1e3..9a1b7c6d1b6 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -25,7 +25,7 @@ module Projects def remove_remaining_members # Remove remaining members and authorizations from source_project - source_project.project_members.destroy_all # rubocop: disable DestroyAll + source_project.project_members.destroy_all # rubocop: disable Cop/DestroyAll end def project_members_in_target_project diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb index c06f572b52f..7aa7ea73639 100644 --- a/app/services/projects/operations/update_service.rb +++ b/app/services/projects/operations/update_service.rb @@ -41,9 +41,9 @@ module Projects attribs = params[:metrics_setting_attributes] return {} unless attribs - destroy = attribs[:external_dashboard_url].blank? + attribs[:external_dashboard_url] = attribs[:external_dashboard_url].presence - { metrics_setting_attributes: attribs.merge(_destroy: destroy) } + { metrics_setting_attributes: attribs } end def error_tracking_params diff --git a/app/services/projects/prometheus/alerts/create_events_service.rb b/app/services/projects/prometheus/alerts/create_events_service.rb index a29240947ff..4fcf841314b 100644 --- a/app/services/projects/prometheus/alerts/create_events_service.rb +++ b/app/services/projects/prometheus/alerts/create_events_service.rb @@ -40,17 +40,13 @@ module Projects def create_managed_prometheus_alert_event(parsed_alert) alert = find_alert(parsed_alert.metric_id) - payload_key = PrometheusAlertEvent.payload_key_for(parsed_alert.metric_id, parsed_alert.starts_at_raw) - - event = PrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, alert, payload_key) + event = PrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, alert, parsed_alert.gitlab_fingerprint) set_status(parsed_alert, event) end def create_self_managed_prometheus_alert_event(parsed_alert) - payload_key = SelfManagedPrometheusAlertEvent.payload_key_for(parsed_alert.starts_at_raw, parsed_alert.title, parsed_alert.full_query) - - event = SelfManagedPrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, payload_key) do |event| + event = SelfManagedPrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, parsed_alert.gitlab_fingerprint) do |event| event.environment = parsed_alert.environment event.title = parsed_alert.title event.query_expression = parsed_alert.full_query diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index 2583a6cae9f..877a4f99a94 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -7,9 +7,19 @@ module Projects include Gitlab::Utils::StrongMemoize include IncidentManagement::Settings + # This set of keys identifies a payload as a valid Prometheus + # payload and thus processable by this service. See also + # https://prometheus.io/docs/alerting/configuration/#webhook_config + REQUIRED_PAYLOAD_KEYS = %w[ + version groupKey status receiver groupLabels commonLabels + commonAnnotations externalURL alerts + ].to_set.freeze + + SUPPORTED_VERSION = '4' + def execute(token) return bad_request unless valid_payload_size? - return unprocessable_entity unless valid_version? + return unprocessable_entity unless self.class.processable?(params) return unauthorized unless valid_alert_manager_token?(token) process_prometheus_alerts @@ -20,6 +30,14 @@ module Projects ServiceResponse.success end + def self.processable?(params) + # Workaround for https://gitlab.com/gitlab-org/gitlab/-/issues/220496 + return false unless params + + REQUIRED_PAYLOAD_KEYS.subset?(params.keys.to_set) && + params['version'] == SUPPORTED_VERSION + end + private def valid_payload_size? @@ -42,12 +60,10 @@ module Projects params['alerts'] end - def valid_version? - params['version'] == '4' - end - def valid_alert_manager_token?(token) - valid_for_manual?(token) || valid_for_managed?(token) + valid_for_manual?(token) || + valid_for_alerts_endpoint?(token) || + valid_for_managed?(token) end def valid_for_manual?(token) @@ -61,6 +77,13 @@ module Projects end end + def valid_for_alerts_endpoint?(token) + return false unless project.alerts_service_activated? + + # Here we are enforcing the existence of the token + compare_token(token, project.alerts_service.token) + end + def valid_for_managed?(token) prometheus_application = available_prometheus_application(project) return false unless prometheus_application diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb index 0483c951f1e..4adcda042d1 100644 --- a/app/services/projects/propagate_service_template.rb +++ b/app/services/projects/propagate_service_template.rb @@ -26,7 +26,7 @@ module Projects def propagate_projects_with_template loop do - batch = Project.uncached { project_ids_batch } + batch = Project.uncached { project_ids_without_integration } bulk_create_from_template(batch) unless batch.empty? @@ -35,40 +35,36 @@ module Projects end def bulk_create_from_template(batch) - service_list = batch.map do |project_id| - service_hash.values << project_id - end + service_list = ServiceList.new(batch, service_hash).to_array Project.transaction do - results = bulk_insert(Service, service_hash.keys << 'project_id', service_list) + results = bulk_insert(*service_list) if data_fields_present? - data_list = results.map { |row| data_hash.values << row['id'] } + data_list = DataList.new(results, data_fields_hash, template.data_fields.class).to_array - bulk_insert(template.data_fields.class, data_hash.keys << 'service_id', data_list) + bulk_insert(*data_list) end run_callbacks(batch) end end - def project_ids_batch - Project.connection.select_values( - <<-SQL - SELECT id - FROM projects - WHERE NOT EXISTS ( - SELECT true - FROM services - WHERE services.project_id = projects.id - AND services.type = #{ActiveRecord::Base.connection.quote(template.type)} - ) - AND projects.pending_delete = false - AND projects.archived = false - LIMIT #{BATCH_SIZE} - SQL - ) + # rubocop: disable CodeReuse/ActiveRecord + def project_ids_without_integration + services = Service + .select('1') + .where('services.project_id = projects.id') + .where(type: template.type) + + Project + .where('NOT EXISTS (?)', services) + .where(pending_delete: false) + .where(archived: false) + .limit(BATCH_SIZE) + .pluck(:id) end + # rubocop: enable CodeReuse/ActiveRecord def bulk_insert(klass, columns, values_array) items_to_insert = values_array.map { |array| Hash[columns.zip(array)] } @@ -77,11 +73,11 @@ module Projects end def service_hash - @service_hash ||= template.as_json(methods: :type, except: %w[id template project_id]) + @service_hash ||= template.to_service_hash end - def data_hash - @data_hash ||= template.data_fields.as_json(only: template.data_fields.class.column_names).except('id', 'service_id') + def data_fields_hash + @data_fields_hash ||= template.to_data_fields_hash end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index e554bed6819..5f8ef75a8d7 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -27,7 +27,11 @@ module Projects remote_mirror.update_start! remote_mirror.ensure_remote! - repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) + + # https://gitlab.com/gitlab-org/gitaly/-/issues/2670 + if Feature.disabled?(:gitaly_ruby_remote_branches_ls_remote) + repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) + end response = remote_mirror.update_repository diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb index 0632df6f6d7..fa8d4c5aa5f 100644 --- a/app/services/projects/update_repository_storage_service.rb +++ b/app/services/projects/update_repository_storage_service.rb @@ -24,7 +24,7 @@ module Projects mark_old_paths_for_archive repository_storage_move.finish! - project.update!(repository_storage: destination_storage_name, repository_read_only: false) + project.leave_pool_repository project.track_project_repository end @@ -34,10 +34,7 @@ module Projects ServiceResponse.success rescue StandardError => e - project.transaction do - repository_storage_move.do_fail! - project.update!(repository_read_only: false) - end + repository_storage_move.do_fail! Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index e10dede632a..58c9bce963b 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -13,8 +13,12 @@ module Projects ensure_wiki_exists if enabling_wiki? - if changing_storage_size? - project.change_repository_storage(params.delete(:repository_storage)) + if changing_repository_storage? + storage_move = project.repository_storage_moves.build( + source_storage_name: project.repository_storage, + destination_storage_name: params.delete(:repository_storage) + ) + storage_move.schedule end yield if block_given? @@ -132,7 +136,7 @@ module Projects def ensure_wiki_exists ProjectWiki.new(project, project.owner).wiki - rescue ProjectWiki::CouldNotCreateWikiError + rescue Wiki::CouldNotCreateWikiError log_error("Could not create wiki for #{project.full_name}") Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki').increment end @@ -145,10 +149,11 @@ module Projects project.previous_changes.include?(:pages_https_only) end - def changing_storage_size? + def changing_repository_storage? new_repository_storage = params[:repository_storage] new_repository_storage && project.repository.exists? && + project.repository_storage != new_repository_storage && can?(current_user, :change_repository_storage, project) end end diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb index cc6ffa9eafc..a0793cff2df 100644 --- a/app/services/projects/update_statistics_service.rb +++ b/app/services/projects/update_statistics_service.rb @@ -5,7 +5,7 @@ module Projects def execute return unless project - Rails.logger.info("Updating statistics for project #{project.id}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("Updating statistics for project #{project.id}") project.statistics.refresh!(only: statistics.map(&:to_sym)) end diff --git a/app/services/prometheus/create_default_alerts_service.rb b/app/services/prometheus/create_default_alerts_service.rb index c87cbbbe3cf..53baf6a650e 100644 --- a/app/services/prometheus/create_default_alerts_service.rb +++ b/app/services/prometheus/create_default_alerts_service.rb @@ -33,6 +33,7 @@ module Prometheus return ServiceResponse.error(message: 'Invalid environment') unless environment create_alerts + schedule_prometheus_update ServiceResponse.success end @@ -51,6 +52,16 @@ module Prometheus end end + def schedule_prometheus_update + return unless prometheus_application + + ::Clusters::Applications::ScheduleUpdateService.new(prometheus_application, project).execute + end + + def prometheus_application + environment.cluster_prometheus_adapter + end + def metrics_by_identifier strong_memoize(:metrics_by_identifier) do metric_identifiers = DEFAULT_ALERTS.map { |alert| alert[:identifier] } diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb index 085cfc76196..e0bc5518d30 100644 --- a/app/services/prometheus/proxy_service.rb +++ b/app/services/prometheus/proxy_service.rb @@ -30,6 +30,10 @@ module Prometheus 'query_range' => { method: ['GET'], params: %w(query start end step timeout) + }, + 'series' => { + method: %w(GET), + params: %w(match start end) } }.freeze diff --git a/app/services/prometheus/proxy_variable_substitution_service.rb b/app/services/prometheus/proxy_variable_substitution_service.rb index 7b98cfc592a..10fb3a8c1b5 100644 --- a/app/services/prometheus/proxy_variable_substitution_service.rb +++ b/app/services/prometheus/proxy_variable_substitution_service.rb @@ -58,7 +58,7 @@ module Prometheus def substitute_variables(result) return success(result) unless query(result) - result[:params][:query] = gsub(query(result), full_context) + result[:params][:query] = gsub(query(result), full_context(result)) success(result) end @@ -75,12 +75,16 @@ module Prometheus end end - def predefined_context - Gitlab::Prometheus::QueryVariables.call(@environment).stringify_keys + def predefined_context(result) + Gitlab::Prometheus::QueryVariables.call( + @environment, + start_time: start_timestamp(result), + end_time: end_timestamp(result) + ).stringify_keys end - def full_context - @full_context ||= predefined_context.reverse_merge(variables_hash) + def full_context(result) + @full_context ||= predefined_context(result).reverse_merge(variables_hash) end def variables @@ -91,6 +95,16 @@ module Prometheus variables.to_h end + def start_timestamp(result) + Time.rfc3339(result[:params][:start]) + rescue ArgumentError + end + + def end_timestamp(result) + Time.rfc3339(result[:params][:end]) + rescue ArgumentError + end + def query(result) result[:params][:query] end diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb index 65dc3297ae8..0cad23f20f7 100644 --- a/app/services/protected_branches/legacy_api_update_service.rb +++ b/app/services/protected_branches/legacy_api_update_service.rb @@ -39,11 +39,11 @@ module ProtectedBranches def delete_redundant_access_levels unless developers_can_merge.nil? - protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll + protected_branch.merge_access_levels.destroy_all # rubocop: disable Cop/DestroyAll end unless developers_can_push.nil? - protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll + protected_branch.push_access_levels.destroy_all # rubocop: disable Cop/DestroyAll end end end diff --git a/app/services/releases/create_evidence_service.rb b/app/services/releases/create_evidence_service.rb new file mode 100644 index 00000000000..ac13dce1729 --- /dev/null +++ b/app/services/releases/create_evidence_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Releases + class CreateEvidenceService + def initialize(release, pipeline: nil) + @release = release + @pipeline = pipeline + end + + def execute + evidence = release.evidences.build + + summary = Evidences::EvidenceSerializer.new.represent(evidence) # rubocop: disable CodeReuse/Serializer + evidence.summary = summary + # TODO: fix the sha generating https://gitlab.com/gitlab-org/gitlab/-/issues/209000 + evidence.summary_sha = Gitlab::CryptoHelper.sha256(summary) + + evidence.save! + end + + private + + attr_reader :release + end +end diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index 81ca9d6d123..92a0b875dd4 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -9,11 +9,16 @@ module Releases return error('Release already exists', 409) if release return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any? + # should be found before the creation of new tag + # because tag creation can spawn new pipeline + # which won't have any data for evidence yet + evidence_pipeline = find_evidence_pipeline + tag = ensure_tag return tag unless tag.is_a?(Gitlab::Git::Tag) - create_release(tag) + create_release(tag, evidence_pipeline) end def find_or_build_release @@ -42,13 +47,15 @@ module Releases Ability.allowed?(current_user, :create_release, project) end - def create_release(tag) + def create_release(tag, evidence_pipeline) release = build_release(tag) release.save! notify_create_release(release) + create_evidence!(release, evidence_pipeline) + success(tag: tag, release: release) rescue => e error(e.message, 400) @@ -70,5 +77,27 @@ module Releases milestones: milestones ) end + + def find_evidence_pipeline + # TODO: remove this with the release creation moved to it's own form https://gitlab.com/gitlab-org/gitlab/-/issues/214245 + return params[:evidence_pipeline] if params[:evidence_pipeline] + + sha = existing_tag&.dereferenced_target&.sha + sha ||= repository.commit(ref)&.sha + + return unless sha + + project.ci_pipelines.for_sha(sha).last + end + + def create_evidence!(release, pipeline) + return if release.historical_release? + + if release.upcoming_release? + CreateEvidenceWorker.perform_at(release.released_at, release.id, pipeline&.id) + else + CreateEvidenceWorker.perform_async(release.id, pipeline&.id) + end + end end end diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb index e0d019f54be..dc23f727079 100644 --- a/app/services/resource_events/change_labels_service.rb +++ b/app/services/resource_events/change_labels_service.rb @@ -22,7 +22,7 @@ module ResourceEvents label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove']) end - Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) + Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert resource.expire_note_etag_cache end diff --git a/app/services/resource_events/change_state_service.rb b/app/services/resource_events/change_state_service.rb new file mode 100644 index 00000000000..8beb76d8aee --- /dev/null +++ b/app/services/resource_events/change_state_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ResourceEvents + class ChangeStateService + attr_reader :resource, :user + + def initialize(user:, resource:) + @user, @resource = user, resource + end + + def execute(state) + ResourceStateEvent.create( + user: user, + issue: issue, + merge_request: merge_request, + state: ResourceStateEvent.states[state], + created_at: Time.zone.now) + + resource.expire_note_etag_cache + end + + private + + def issue + return unless resource.is_a?(Issue) + + resource + end + + def merge_request + return unless resource.is_a?(MergeRequest) + + resource + end + end +end diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb index 4aa9bb80229..122bcb8550f 100644 --- a/app/services/resource_events/merge_into_notes_service.rb +++ b/app/services/resource_events/merge_into_notes_service.rb @@ -11,7 +11,8 @@ module ResourceEvents SYNTHETIC_NOTE_BUILDER_SERVICES = [ SyntheticLabelNotesBuilderService, - SyntheticMilestoneNotesBuilderService + SyntheticMilestoneNotesBuilderService, + SyntheticStateNotesBuilderService ].freeze attr_reader :resource, :current_user, :params @@ -23,7 +24,7 @@ module ResourceEvents end def execute(notes = []) - (notes + synthetic_notes).sort_by { |n| n.created_at } + (notes + synthetic_notes).sort_by(&:created_at) end private diff --git a/app/services/resource_events/synthetic_state_notes_builder_service.rb b/app/services/resource_events/synthetic_state_notes_builder_service.rb new file mode 100644 index 00000000000..763134d98d8 --- /dev/null +++ b/app/services/resource_events/synthetic_state_notes_builder_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ResourceEvents + class SyntheticStateNotesBuilderService < BaseSyntheticNotesBuilderService + private + + def synthetic_notes + state_change_events.map do |event| + StateNote.from_event(event, resource: resource, resource_parent: resource_parent) + end + end + + def state_change_events + return [] unless resource.respond_to?(:resource_state_events) + + events = resource.resource_state_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord + since_fetch_at(events) + end + end +end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index bf21eba28f7..650dc197f8c 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -5,7 +5,6 @@ class SearchService SEARCH_TERM_LIMIT = 64 SEARCH_CHAR_LIMIT = 4096 - DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE MAX_PER_PAGE = 200 @@ -62,8 +61,8 @@ class SearchService @search_results ||= search_service.execute end - def search_objects - @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page)) + def search_objects(preload_method = nil) + @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method)) end private @@ -83,16 +82,21 @@ class SearchService end def redact_unauthorized_results(results_collection) - results = results_collection.to_a - permitted_results = results.select { |object| visible_result?(object) } + redacted_results = results_collection.reject { |object| visible_result?(object) } + + if redacted_results.any? + redacted_log = redacted_results.each_with_object({}) do |object, memo| + memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name } + end + + log_redacted_search_results(redacted_log.values) - redacted_results = (results - permitted_results).each_with_object({}) do |object, memo| - memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name } + return results_collection.id_not_in(redacted_log.keys) if results_collection.is_a?(ActiveRecord::Relation) end - log_redacted_search_results(redacted_results.values) if redacted_results.any? + return results_collection if results_collection.is_a?(ActiveRecord::Relation) - return results_collection.id_not_in(redacted_results.keys) if results_collection.is_a?(ActiveRecord::Relation) + permitted_results = results_collection - redacted_results Kaminari.paginate_array( permitted_results, diff --git a/app/services/service_response.rb b/app/services/service_response.rb index 08b7e9d0831..74c0be22d46 100644 --- a/app/services/service_response.rb +++ b/app/services/service_response.rb @@ -26,6 +26,12 @@ class ServiceResponse status == :error end + def errors + return [] unless error? + + Array.wrap(message) + end + private attr_writer :status, :message, :http_status, :payload diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb index 81d12997335..5d1fe815d83 100644 --- a/app/services/snippets/base_service.rb +++ b/app/services/snippets/base_service.rb @@ -6,12 +6,13 @@ module Snippets CreateRepositoryError = Class.new(StandardError) - attr_reader :uploaded_files + attr_reader :uploaded_assets, :snippet_files def initialize(project, user = nil, params = {}) super - @uploaded_files = Array(@params.delete(:files).presence) + @uploaded_assets = Array(@params.delete(:files).presence) + @snippet_files = SnippetInputActionCollection.new(Array(@params.delete(:snippet_files).presence)) filter_spam_check_params end @@ -22,12 +23,30 @@ module Snippets Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level) end - def error_forbidden_visibility(snippet) + def forbidden_visibility_error(snippet) deny_visibility_level(snippet) snippet_error_response(snippet, 403) end + def valid_params? + return true if snippet_files.empty? + + (params.keys & [:content, :file_name]).none? && snippet_files.valid? + end + + def invalid_params_error(snippet) + if snippet_files.valid? + [:content, :file_name].each do |key| + snippet.errors.add(key, 'and snippet files cannot be used together') if params.key?(key) + end + else + snippet.errors.add(:snippet_files, 'have invalid data') + end + + snippet_error_response(snippet, 403) + end + def snippet_error_response(snippet, http_status) ServiceResponse.error( message: snippet.errors.full_messages.to_sentence, @@ -52,5 +71,13 @@ module Snippets message end + + def files_to_commit(snippet) + snippet_files.to_commit_actions.presence || build_actions_from_params(snippet) + end + + def build_actions_from_params(snippet) + raise NotImplementedError + end end end diff --git a/app/services/snippets/bulk_destroy_service.rb b/app/services/snippets/bulk_destroy_service.rb index d9cc383a5a6..a612d8f8dfc 100644 --- a/app/services/snippets/bulk_destroy_service.rb +++ b/app/services/snippets/bulk_destroy_service.rb @@ -14,12 +14,12 @@ module Snippets @snippets = snippets end - def execute + def execute(options = {}) return ServiceResponse.success(message: 'No snippets found.') if snippets.empty? - user_can_delete_snippets! + user_can_delete_snippets! unless options[:hard_delete] attempt_delete_repositories! - snippets.destroy_all # rubocop: disable DestroyAll + snippets.destroy_all # rubocop: disable Cop/DestroyAll ServiceResponse.success(message: 'Snippets were deleted.') rescue SnippetAccessError diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb index ed6da3a0ad0..7b477621da3 100644 --- a/app/services/snippets/create_service.rb +++ b/app/services/snippets/create_service.rb @@ -5,13 +5,15 @@ module Snippets def execute @snippet = build_from_params + return invalid_params_error(@snippet) unless valid_params? + unless visibility_allowed?(@snippet, @snippet.visibility_level) - return error_forbidden_visibility(@snippet) + return forbidden_visibility_error(@snippet) end @snippet.author = current_user - spam_check(@snippet, current_user) + spam_check(@snippet, current_user, action: :create) if save_and_commit UserAgentDetailService.new(@snippet, @request).create @@ -29,12 +31,21 @@ module Snippets def build_from_params if project - project.snippets.build(params) + project.snippets.build(create_params) else - PersonalSnippet.new(params) + PersonalSnippet.new(create_params) end end + # If the snippet_files param is present + # we need to fill content and file_name from + # the model + def create_params + return params if snippet_files.empty? + + params.merge(content: snippet_files[0].content, file_name: snippet_files[0].file_path) + end + def save_and_commit snippet_saved = @snippet.save @@ -75,19 +86,19 @@ module Snippets message: 'Initial commit' } - @snippet.snippet_repository.multi_files_action(current_user, snippet_files, commit_attrs) - end - - def snippet_files - [{ file_path: params[:file_name], content: params[:content] }] + @snippet.snippet_repository.multi_files_action(current_user, files_to_commit(@snippet), commit_attrs) end def move_temporary_files return unless @snippet.is_a?(PersonalSnippet) - uploaded_files.each do |file| + uploaded_assets.each do |file| FileMover.new(file, from_model: current_user, to_model: @snippet).execute end end + + def build_actions_from_params(_snippet) + [{ file_path: params[:file_name], content: params[:content] }] + end end end diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb index 250120c1c19..6cdc2c374da 100644 --- a/app/services/snippets/update_service.rb +++ b/app/services/snippets/update_service.rb @@ -7,12 +7,14 @@ module Snippets UpdateError = Class.new(StandardError) def execute(snippet) + return invalid_params_error(snippet) unless valid_params? + if visibility_changed?(snippet) && !visibility_allowed?(snippet, visibility_level) - return error_forbidden_visibility(snippet) + return forbidden_visibility_error(snippet) end - snippet.assign_attributes(params) - spam_check(snippet, current_user) + update_snippet_attributes(snippet) + spam_check(snippet, current_user, action: :update) if save_and_commit(snippet) Gitlab::UsageDataCounters::SnippetCounter.count(:update) @@ -29,6 +31,19 @@ module Snippets visibility_level && visibility_level.to_i != snippet.visibility_level end + def update_snippet_attributes(snippet) + # We can remove the following condition once + # https://gitlab.com/gitlab-org/gitlab/-/issues/217801 + # is implemented. + # Once we can perform different operations through this service + # we won't need to keep track of the `content` and `file_name` fields + if snippet_files.any? + params.merge!(content: snippet_files[0].content, file_name: snippet_files[0].file_path) + end + + snippet.assign_attributes(params) + end + def save_and_commit(snippet) return false unless snippet.save @@ -81,15 +96,7 @@ module Snippets message: 'Update snippet' } - snippet.snippet_repository.multi_files_action(current_user, snippet_files(snippet), commit_attrs) - end - - def snippet_files(snippet) - file_name_on_repo = snippet.file_name_on_repo - - [{ previous_path: file_name_on_repo, - file_path: params[:file_name] || file_name_on_repo, - content: params[:content] }] + snippet.snippet_repository.multi_files_action(current_user, files_to_commit(snippet), commit_attrs) end # Because we are removing repositories we don't want to remove @@ -101,7 +108,15 @@ module Snippets end def committable_attributes? - (params.stringify_keys.keys & COMMITTABLE_ATTRIBUTES).present? + (params.stringify_keys.keys & COMMITTABLE_ATTRIBUTES).present? || snippet_files.any? + end + + def build_actions_from_params(snippet) + file_name_on_repo = snippet.file_name_on_repo + + [{ previous_path: file_name_on_repo, + file_path: params[:file_name] || file_name_on_repo, + content: params[:content] }] end end end diff --git a/app/services/spam/akismet_service.rb b/app/services/spam/akismet_service.rb index ab35fb8700f..e11a1dbdd96 100644 --- a/app/services/spam/akismet_service.rb +++ b/app/services/spam/akismet_service.rb @@ -27,7 +27,7 @@ module Spam is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params) is_spam || is_blatant rescue => e - Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Unable to connect to Akismet: #{e}, skipping check") false end end @@ -67,7 +67,7 @@ module Spam akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend true rescue => e - Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Unable to connect to Akismet: #{e}, skipping!") false end end diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb index f0a4aff4443..b745b67f566 100644 --- a/app/services/spam/spam_action_service.rb +++ b/app/services/spam/spam_action_service.rb @@ -7,9 +7,11 @@ module Spam attr_accessor :target, :request, :options attr_reader :spam_log - def initialize(spammable:, request:) + def initialize(spammable:, request:, user:, context: {}) @target = spammable @request = request + @user = user + @context = context @options = {} if @request @@ -22,7 +24,7 @@ module Spam end end - def execute(api: false, recaptcha_verified:, spam_log_id:, user:) + def execute(api: false, recaptcha_verified:, spam_log_id:) if recaptcha_verified # If it's a request which is already verified through reCAPTCHA, # update the spam log accordingly. @@ -40,6 +42,8 @@ module Spam private + attr_reader :user, :context + def allowlisted?(user) user.respond_to?(:gitlab_employee) && user.gitlab_employee? end @@ -49,7 +53,8 @@ module Spam # ask the SpamVerdictService what to do with the target. spam_verdict_service.execute.tap do |result| case result - when REQUIRE_RECAPTCHA + when CONDITIONAL_ALLOW + # at the moment, this means "ask for reCAPTCHA" create_spam_log(api) break if target.allow_possible_spam? @@ -74,7 +79,7 @@ module Spam description: target.spam_description, source_ip: options[:ip_address], user_agent: options[:user_agent], - noteable_type: target.class.to_s, + noteable_type: notable_type, via_api: api } ) @@ -84,8 +89,14 @@ module Spam def spam_verdict_service SpamVerdictService.new(target: target, + user: user, request: @request, - options: options) + options: options, + context: context.merge(target_type: notable_type)) + end + + def notable_type + @notable_type ||= target.class.to_s end end end diff --git a/app/services/spam/spam_constants.rb b/app/services/spam/spam_constants.rb index 085bac684c4..2a16cfae78b 100644 --- a/app/services/spam/spam_constants.rb +++ b/app/services/spam/spam_constants.rb @@ -2,8 +2,24 @@ module Spam module SpamConstants - REQUIRE_RECAPTCHA = :recaptcha - DISALLOW = :disallow - ALLOW = :allow + CONDITIONAL_ALLOW = "conditional_allow" + DISALLOW = "disallow" + ALLOW = "allow" + BLOCK_USER = "block" + + SUPPORTED_VERDICTS = { + BLOCK_USER => { + priority: 1 + }, + DISALLOW => { + priority: 2 + }, + CONDITIONAL_ALLOW => { + priority: 3 + }, + ALLOW => { + priority: 4 + } + }.freeze end end diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 2b4d5f4a984..68f1135ae28 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -5,22 +5,90 @@ module Spam include AkismetMethods include SpamConstants - def initialize(target:, request:, options:) + def initialize(user:, target:, request:, options:, context: {}) @target = target @request = request + @user = user @options = options + @verdict_params = assemble_verdict_params(context) end def execute + external_spam_check_result = spam_verdict + akismet_result = akismet_verdict + + # filter out anything we don't recognise, including nils. + valid_results = [external_spam_check_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) } + # Treat nils - such as service unavailable - as ALLOW + return ALLOW unless valid_results.any? + + # Favour the most restrictive result. + valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] } + end + + private + + attr_reader :user, :target, :request, :options, :verdict_params + + def akismet_verdict if akismet.spam? - Gitlab::Recaptcha.enabled? ? REQUIRE_RECAPTCHA : DISALLOW + Gitlab::Recaptcha.enabled? ? CONDITIONAL_ALLOW : DISALLOW else ALLOW end end - private + def spam_verdict + return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled + return if endpoint_url.blank? + + begin + result = Gitlab::HTTP.post(endpoint_url, body: verdict_params.to_json, headers: { 'Content-Type' => 'application/json' }) + return unless result + + json_result = Gitlab::Json.parse(result).with_indifferent_access + # @TODO metrics/logging + # Expecting: + # error: (string or nil) + # result: (string or nil) + verdict = json_result[:verdict] + return unless SUPPORTED_VERDICTS.include?(verdict) - attr_reader :target, :request, :options + # @TODO log if json_result[:error] + + json_result[:verdict] + rescue *Gitlab::HTTP::HTTP_ERRORS => e + # @TODO: log error via try_post https://gitlab.com/gitlab-org/gitlab/-/issues/219223 + Gitlab::ErrorTracking.log_exception(e) + return + rescue + # @TODO log + ALLOW + end + end + + def assemble_verdict_params(context) + return {} unless endpoint_url.present? + + project = target.try(:project) + + context.merge({ + target: { + title: target.spam_title, + description: target.spam_description, + type: target.class.to_s + }, + user: { + created_at: user.created_at, + email: user.email, + username: user.username + }, + user_in_project: user.authorized_project?(project) + }) + end + + def endpoint_url + @endpoint_url ||= Gitlab::CurrentSettings.current_application_settings.spam_check_endpoint_url + end end end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 3265eb106eb..4bbde3a9648 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -28,7 +28,7 @@ class SubmitUsagePingService true rescue Gitlab::HTTP::Error => e - Rails.logger.info "Unable to contact GitLab, Inc.: #{e}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("Unable to contact GitLab, Inc.: #{e}") false end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index 479eed3ce57..ab80b23a37b 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -2,109 +2,49 @@ module Suggestions class ApplyService < ::BaseService - DEFAULT_SUGGESTION_COMMIT_MESSAGE = 'Apply suggestion to %{file_path}' - - PLACEHOLDERS = { - 'project_path' => ->(suggestion, user) { suggestion.project.path }, - 'project_name' => ->(suggestion, user) { suggestion.project.name }, - 'file_path' => ->(suggestion, user) { suggestion.file_path }, - 'branch_name' => ->(suggestion, user) { suggestion.branch }, - 'username' => ->(suggestion, user) { user.username }, - 'user_full_name' => ->(suggestion, user) { user.name } - }.freeze - - # This regex is built dynamically using the keys from the PLACEHOLDER struct. - # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash. - # This regex will build the new PLACEHOLDER_REGEX with the new information - PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map { |key| Regexp.new(Regexp.escape(key)) }).freeze - - attr_reader :current_user - - def initialize(current_user) + def initialize(current_user, *suggestions) @current_user = current_user + @suggestion_set = Gitlab::Suggestions::SuggestionSet.new(suggestions) end - def execute(suggestion) - unless suggestion.appliable?(cached: false) - return error('Suggestion is not appliable') - end - - unless latest_source_head?(suggestion) - return error('The file has been changed') + def execute + if suggestion_set.valid? + result + else + error(suggestion_set.error_message) end + end - diff_file = suggestion.diff_file - - unless diff_file - return error('The file was not found') - end + private - params = file_update_params(suggestion, diff_file) - result = ::Files::UpdateService.new(suggestion.project, current_user, params).execute + attr_reader :current_user, :suggestion_set - if result[:status] == :success - suggestion.update(commit_id: result[:result], applied: true) + def result + multi_service.execute.tap do |result| + update_suggestions(result) end - - result - rescue Files::UpdateService::FileChangedError - error('The file has been changed') end - private + def update_suggestions(result) + return unless result[:status] == :success - # Checks whether the latest source branch HEAD matches with - # the position HEAD we're using to update the file content. Since - # the persisted HEAD is updated async (for MergeRequest), - # it's more consistent to fetch this data directly from the - # repository. - def latest_source_head?(suggestion) - suggestion.position.head_sha == suggestion.noteable.source_branch_sha + Suggestion.id_in(suggestion_set.suggestions) + .update_all(commit_id: result[:result], applied: true) end - def file_update_params(suggestion, diff_file) - blob = diff_file.new_blob - project = suggestion.project - file_path = suggestion.file_path - branch_name = suggestion.branch - file_content = new_file_content(suggestion, blob) - commit_message = processed_suggestion_commit_message(suggestion) - - file_last_commit = - Gitlab::Git::Commit.last_for_path(project.repository, - blob.commit_id, - blob.path) - - { - file_path: file_path, - branch_name: branch_name, - start_branch: branch_name, + def multi_service + params = { commit_message: commit_message, - file_content: file_content, - last_commit_sha: file_last_commit&.id + branch_name: suggestion_set.branch, + start_branch: suggestion_set.branch, + actions: suggestion_set.actions } - end - - def new_file_content(suggestion, blob) - range = suggestion.from_line_index..suggestion.to_line_index - blob.load_all_data! - content = blob.data.lines - content[range] = suggestion.to_content - - content.join - end - - def suggestion_commit_message(project) - project.suggestion_commit_message.presence || DEFAULT_SUGGESTION_COMMIT_MESSAGE + ::Files::MultiService.new(suggestion_set.project, current_user, params) end - def processed_suggestion_commit_message(suggestion) - message = suggestion_commit_message(suggestion.project) - - Gitlab::StringPlaceholderReplacer.replace_string_placeholders(message, PLACEHOLDERS_REGEX) do |key| - PLACEHOLDERS[key].call(suggestion, current_user) - end + def commit_message + Gitlab::Suggestions::CommitMessage.new(current_user, suggestion_set).message end end end diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb index 1d3338c1b45..93d2bd11426 100644 --- a/app/services/suggestions/create_service.rb +++ b/app/services/suggestions/create_service.rb @@ -25,7 +25,7 @@ module Suggestions end rows.in_groups_of(100, false) do |rows| - Gitlab::Database.bulk_insert('suggestions', rows) + Gitlab::Database.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert end end end diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 275c64bea89..7d7ee8d829e 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -128,7 +128,7 @@ module SystemNotes body = cross_reference_note_content(gfm_reference) if noteable.is_a?(ExternalIssue) - noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author) + noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author) else create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference')) end @@ -225,7 +225,12 @@ module SystemNotes action = status == 'reopened' ? 'opened' : status - create_note(NoteSummary.new(noteable, project, author, body, action: action)) + # A state event which results in a synthetic note will be + # created by EventCreateService if change event tracking + # is enabled. + unless state_change_tracking_enabled? + create_note(NoteSummary.new(noteable, project, author, body, action: action)) + end end # Check if a cross reference to a noteable from a mentioner already exists @@ -318,6 +323,11 @@ module SystemNotes def self.cross_reference?(note_text) note_text =~ /\A#{cross_reference_note_prefix}/i end + + def state_change_tracking_enabled? + noteable.respond_to?(:resource_state_events) && + ::Feature.enabled?(:track_resource_state_change_events, noteable.project) + end end end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index ebebf29c28b..0fda6fb1ed0 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -2,6 +2,8 @@ module TestHooks class BaseService + include BaseServiceUtility + attr_accessor :hook, :current_user, :trigger def initialize(hook, current_user, trigger) @@ -12,31 +14,11 @@ module TestHooks def execute trigger_key = hook.class.triggers.key(trigger.to_sym) - trigger_data_method = "#{trigger}_data" - - if trigger_key.nil? || !self.respond_to?(trigger_data_method, true) - return error('Testing not available for this hook') - end - - error_message = catch(:validation_error) do # rubocop:disable Cop/BanCatchThrow - sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend - - return hook.execute(sample_data, trigger_key) # rubocop:disable Cop/AvoidReturnFromBlocks - end - - error(error_message) - end - - private - def error(message, http_status = nil) - result = { - message: message, - status: :error - } + return error('Testing not available for this hook') if trigger_key.nil? || data.blank? + return error(data[:error]) if data[:error].present? - result[:http_status] = http_status if http_status - result + hook.execute(data, trigger_key) end end end diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb index aa80cc928b9..4e554dce357 100644 --- a/app/services/test_hooks/project_service.rb +++ b/app/services/test_hooks/project_service.rb @@ -2,6 +2,9 @@ module TestHooks class ProjectService < TestHooks::BaseService + include Integrations::ProjectTestData + include Gitlab::Utils::StrongMemoize + attr_writer :project def project @@ -10,58 +13,25 @@ module TestHooks private - def push_events_data - throw(:validation_error, s_('TestHooks|Ensure the project has at least one commit.')) if project.empty_repo? # rubocop:disable Cop/BanCatchThrow - - Gitlab::DataBuilder::Push.build_sample(project, current_user) - end - - alias_method :tag_push_events_data, :push_events_data - - def note_events_data - note = project.notes.first - throw(:validation_error, s_('TestHooks|Ensure the project has notes.')) unless note.present? # rubocop:disable Cop/BanCatchThrow - - Gitlab::DataBuilder::Note.build(note, current_user) - end - - def issues_events_data - issue = project.issues.first - throw(:validation_error, s_('TestHooks|Ensure the project has issues.')) unless issue.present? # rubocop:disable Cop/BanCatchThrow - - issue.to_hook_data(current_user) - end - - alias_method :confidential_issues_events_data, :issues_events_data - - def merge_requests_events_data - merge_request = project.merge_requests.first - throw(:validation_error, s_('TestHooks|Ensure the project has merge requests.')) unless merge_request.present? # rubocop:disable Cop/BanCatchThrow - - merge_request.to_hook_data(current_user) - end - - def job_events_data - build = project.builds.first - throw(:validation_error, s_('TestHooks|Ensure the project has CI jobs.')) unless build.present? # rubocop:disable Cop/BanCatchThrow - - Gitlab::DataBuilder::Build.build(build) - end - - def pipeline_events_data - pipeline = project.ci_pipelines.first - throw(:validation_error, s_('TestHooks|Ensure the project has CI pipelines.')) unless pipeline.present? # rubocop:disable Cop/BanCatchThrow - - Gitlab::DataBuilder::Pipeline.build(pipeline) - end - - def wiki_page_events_data - page = project.wiki.list_pages(limit: 1).first - if !project.wiki_enabled? || page.blank? - throw(:validation_error, s_('TestHooks|Ensure the wiki is enabled and has pages.')) # rubocop:disable Cop/BanCatchThrow + def data + strong_memoize(:data) do + case trigger + when 'push_events', 'tag_push_events' + push_events_data + when 'note_events' + note_events_data + when 'issues_events', 'confidential_issues_events' + issues_events_data + when 'merge_requests_events' + merge_requests_events_data + when 'job_events' + job_events_data + when 'pipeline_events' + pipeline_events_data + when 'wiki_page_events' + wiki_page_events_data + end end - - Gitlab::DataBuilder::WikiPage.build(page, current_user, 'create') end end end diff --git a/app/services/test_hooks/system_service.rb b/app/services/test_hooks/system_service.rb index 5c7961f417d..66d78bfc578 100644 --- a/app/services/test_hooks/system_service.rb +++ b/app/services/test_hooks/system_service.rb @@ -2,23 +2,26 @@ module TestHooks class SystemService < TestHooks::BaseService - private - - def push_events_data - Gitlab::DataBuilder::Push.sample_data - end + include Gitlab::Utils::StrongMemoize - def tag_push_events_data - Gitlab::DataBuilder::Push.sample_data - end + private - def repository_update_events_data - Gitlab::DataBuilder::Repository.sample_data + def data + strong_memoize(:data) do + case trigger + when 'push_events', 'tag_push_events' + Gitlab::DataBuilder::Push.sample_data + when 'repository_update_events' + Gitlab::DataBuilder::Repository.sample_data + when 'merge_requests_events' + merge_requests_events_data + end + end end def merge_requests_events_data merge_request = MergeRequest.of_projects(current_user.projects.select(:id)).first - throw(:validation_error, s_('TestHooks|Ensure one of your projects has merge requests.')) unless merge_request.present? # rubocop:disable Cop/BanCatchThrow + return { error: s_('TestHooks|Ensure one of your projects has merge requests.') } unless merge_request.present? merge_request.to_hook_data(current_user) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 55f888d5664..e6fb0d3c72e 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -30,7 +30,7 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def close_issue(issue, current_user) - mark_pending_todos_as_done(issue, current_user) + resolve_todos_for_target(issue, current_user) end # When we destroy a todo target we should: @@ -79,7 +79,7 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def close_merge_request(merge_request, current_user) - mark_pending_todos_as_done(merge_request, current_user) + resolve_todos_for_target(merge_request, current_user) end # When merge a merge request we should: @@ -87,7 +87,7 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def merge_merge_request(merge_request, current_user) - mark_pending_todos_as_done(merge_request, current_user) + resolve_todos_for_target(merge_request, current_user) end # When a build fails on the HEAD of a merge request we should: @@ -105,7 +105,7 @@ class TodoService # * mark all pending todos related to the merge request for that user as done # def merge_request_push(merge_request, current_user) - mark_pending_todos_as_done(merge_request, current_user) + resolve_todos_for_target(merge_request, current_user) end # When a build is retried to a merge request we should: @@ -114,7 +114,7 @@ class TodoService # def merge_request_build_retried(merge_request) merge_request.merge_participants.each do |user| - mark_pending_todos_as_done(merge_request, user) + resolve_todos_for_target(merge_request, user) end end @@ -151,76 +151,68 @@ class TodoService # * mark all pending todos related to the awardable for the current user as done # def new_award_emoji(awardable, current_user) - mark_pending_todos_as_done(awardable, current_user) + resolve_todos_for_target(awardable, current_user) end - # When marking pending todos as done we should: + # When assigning an alert we should: # - # * mark all pending todos related to the target for the current user as done + # * create a pending todo for new assignee if alert is assigned # - def mark_pending_todos_as_done(target, user) - attributes = attributes_for_target(target) - pending_todos(user, attributes).update_all(state: :done) - user.update_todos_count_cache + def assign_alert(alert, current_user) + create_assignment_todo(alert, current_user, []) end - # When user marks some todos as done - def mark_todos_as_done(todos, current_user) - update_todos_state(todos, current_user, :done) + # When user marks an issue as todo + def mark_todo(issuable, current_user) + attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED) + create_todos(current_user, attributes) end - def mark_todos_as_done_by_ids(ids, current_user) - todos = todos_by_ids(ids, current_user) - mark_todos_as_done(todos, current_user) + def todo_exist?(issuable, current_user) + TodosFinder.new(current_user).any_for_target?(issuable, :pending) end - def mark_all_todos_as_done_by_user(current_user) - todos = TodosFinder.new(current_user).execute - mark_todos_as_done(todos, current_user) - end + # Resolves all todos related to target + def resolve_todos_for_target(target, current_user) + attributes = attributes_for_target(target) - def mark_todo_as_done(todo, current_user) - return if todo.done? + resolve_todos(pending_todos(current_user, attributes), current_user) + end - todo.update(state: :done) + def resolve_todos(todos, current_user, resolution: :done, resolved_by_action: :system_done) + todos_ids = todos.batch_update(state: resolution, resolved_by_action: resolved_by_action) current_user.update_todos_count_cache - end - # When user marks some todos as pending - def mark_todos_as_pending(todos, current_user) - update_todos_state(todos, current_user, :pending) + todos_ids end - def mark_todos_as_pending_by_ids(ids, current_user) - todos = todos_by_ids(ids, current_user) - mark_todos_as_pending(todos, current_user) - end + def resolve_todo(todo, current_user, resolution: :done, resolved_by_action: :system_done) + return if todo.done? - # When user marks an issue as todo - def mark_todo(issuable, current_user) - attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED) - create_todos(current_user, attributes) - end + todo.update(state: resolution, resolved_by_action: resolved_by_action) - def todo_exist?(issuable, current_user) - TodosFinder.new(current_user).any_for_target?(issuable, :pending) + current_user.update_todos_count_cache end - private + def restore_todos(todos, current_user) + todos_ids = todos.batch_update(state: :pending) - def todos_by_ids(ids, current_user) - current_user.todos_limited_to(Array(ids)) + current_user.update_todos_count_cache + + todos_ids end - def update_todos_state(todos, current_user, state) - todos_ids = todos.update_state(state) + def restore_todo(todo, current_user) + return if todo.pending? - current_user.update_todos_count_cache + todo.update(state: :pending) - todos_ids + current_user.update_todos_count_cache end + private + def create_todos(users, attributes) Array(users).map do |user| next if pending_todos(user, attributes).exists? @@ -252,16 +244,16 @@ class TodoService return unless note.can_create_todo? project = note.project - target = note.noteable + target = note.noteable - mark_pending_todos_as_done(target, author) + resolve_todos_for_target(target, author) create_mention_todos(project, target, author, note, skip_users) end - def create_assignment_todo(issuable, author, old_assignees = []) - if issuable.assignees.any? - assignees = issuable.assignees - old_assignees - attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED) + def create_assignment_todo(target, author, old_assignees = []) + if target.assignees.any? + assignees = target.assignees - old_assignees + attributes = attributes_for_todo(target.project, target, author, Todo::ASSIGNED) create_todos(assignees, attributes) end end diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb index 66f1ccfab70..11727f05f35 100644 --- a/app/services/user_project_access_changed_service.rb +++ b/app/services/user_project_access_changed_service.rb @@ -19,7 +19,8 @@ class UserProjectAccessChangedService if priority == HIGH_PRIORITY AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext else - AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in(DELAY, bulk_args) # rubocop:disable Scalability/BulkPerformWithContext + AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in( # rubocop:disable Scalability/BulkPerformWithContext + DELAY, bulk_args, batch_size: 100, batch_delay: 30.seconds) end end end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 3938d675596..f06f00a5c3f 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -82,7 +82,8 @@ module Users :organization, :location, :public_email, - :user_type + :user_type, + :note ] end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 587a8516394..436d4fb3985 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -56,7 +56,7 @@ module Users MigrateToGhostUserService.new(user).execute unless options[:hard_delete] - response = Snippets::BulkDestroyService.new(current_user, user.snippets).execute + response = Snippets::BulkDestroyService.new(current_user, user.snippets).execute(options) raise DestroyError, response.message if response.error? # Rails attempts to load all related records into memory before diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb index 5ca9ed67e56..1b46edd4d7d 100644 --- a/app/services/users/migrate_to_ghost_user_service.rb +++ b/app/services/users/migrate_to_ghost_user_service.rb @@ -53,6 +53,7 @@ module Users migrate_abuse_reports migrate_award_emoji migrate_snippets + migrate_reviews end # rubocop: disable CodeReuse/ActiveRecord @@ -85,6 +86,10 @@ module Users snippets = user.snippets.only_project_snippets snippets.update_all(author_id: ghost_user.id) end + + def migrate_reviews + user.reviews.update_all(author_id: ghost_user.id) + end end end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 178a321e20c..91a26ff45b1 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -63,7 +63,7 @@ class WebHookService error_message: e.to_s ) - Rails.logger.error("WebHook Error => #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("WebHook Error => #{e}") { status: :error, diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb index 4ef19676d82..63107445782 100644 --- a/app/services/wiki_pages/create_service.rb +++ b/app/services/wiki_pages/create_service.rb @@ -22,7 +22,7 @@ module WikiPages end def event_action - Event::CREATED + :created end end end diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb index eb162223723..d59c27bb92a 100644 --- a/app/services/wiki_pages/destroy_service.rb +++ b/app/services/wiki_pages/destroy_service.rb @@ -19,7 +19,7 @@ module WikiPages end def event_action - Event::DESTROYED + :destroyed end end end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb index 0a056f1ec33..5ac6902e0b0 100644 --- a/app/services/wiki_pages/update_service.rb +++ b/app/services/wiki_pages/update_service.rb @@ -22,7 +22,7 @@ module WikiPages end def event_action - Event::UPDATED + :updated end def slug_for_page(page) |