diff options
Diffstat (limited to 'app/services')
116 files changed, 1218 insertions, 1366 deletions
diff --git a/app/services/achievements/update_user_achievement_priorities_service.rb b/app/services/achievements/update_user_achievement_priorities_service.rb new file mode 100644 index 00000000000..1165a1b3bf6 --- /dev/null +++ b/app/services/achievements/update_user_achievement_priorities_service.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Achievements + class UpdateUserAchievementPrioritiesService + attr_reader :current_user, :user_achievements + + def initialize(current_user, user_achievements) + @current_user = current_user + @user_achievements = user_achievements + end + + def execute + return error_no_permissions unless allowed? + + prioritized_user_achievements_map = Hash[user_achievements.map.with_index { |ua, idx| [ua.id, idx] }] + + user_achievements_priorities_mapping = current_user.user_achievements.each_with_object({}) do |ua, result| + next if ua.priority.nil? && !prioritized_user_achievements_map.key?(ua.id) + + result[ua] = { priority: prioritized_user_achievements_map.fetch(ua.id, nil) } + end + + return ServiceResponse.success(payload: []) if user_achievements_priorities_mapping.empty? + + ::Gitlab::Database::BulkUpdate.execute(%i[priority], user_achievements_priorities_mapping) + + ServiceResponse.success(payload: user_achievements_priorities_mapping.keys.map(&:reload)) + end + + private + + def allowed? + user_achievements.all? { |user_achievement| current_user&.can?(:update_owned_user_achievement, user_achievement) } + end + + def error(message) + ServiceResponse.error(payload: user_achievements, message: Array(message)) + end + + def error_no_permissions + error("You can't update at least one of the given user achievements.") + end + end +end diff --git a/app/services/admin/abuse_reports/moderate_user_service.rb b/app/services/admin/abuse_reports/moderate_user_service.rb index 823568d9db8..1e14806c694 100644 --- a/app/services/admin/abuse_reports/moderate_user_service.rb +++ b/app/services/admin/abuse_reports/moderate_user_service.rb @@ -42,6 +42,7 @@ module Admin when :block_user then block_user when :delete_user then delete_user when :close_report then close_report + when :trust_user then trust_user end end @@ -66,6 +67,10 @@ module Admin success end + def trust_user + Users::TrustService.new(current_user).execute(abuse_report.user) + end + def close_similar_open_reports # admins see the abuse report and other open reports for the same user in one page # hence, if the request is to close the report, close other open reports for the same user too diff --git a/app/services/audit_events/build_service.rb b/app/services/audit_events/build_service.rb index f5322fa5ff4..9eab2f836db 100644 --- a/app/services/audit_events/build_service.rb +++ b/app/services/audit_events/build_service.rb @@ -11,7 +11,10 @@ module AuditEvents def initialize( author:, scope:, target:, message:, created_at: DateTime.current, additional_details: {}, ip_address: nil, target_details: nil) - raise MissingAttributeError if missing_attribute?(author, scope, target, message) + raise MissingAttributeError, "author" if author.blank? + raise MissingAttributeError, "scope" if scope.blank? + raise MissingAttributeError, "target" if target.blank? + raise MissingAttributeError, "message" if message.blank? @author = build_author(author) @scope = scope @@ -32,10 +35,6 @@ module AuditEvents private - def missing_attribute?(author, scope, target, message) - author.blank? || scope.blank? || target.blank? || message.blank? - end - def payload base_payload.merge(details: base_details_payload) end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 9b010272995..363510a41a1 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -39,32 +39,45 @@ module Auth end def self.full_access_token(*names) - access_token(%w[*], names) + names_and_actions = names.index_with { %w[*] } + access_token(names_and_actions) end def self.import_access_token - access_token(%w[*], ['import'], 'registry') + access_token({ 'import' => %w[*] }, 'registry') end def self.pull_access_token(*names) - access_token(['pull'], names) + names_and_actions = names.index_with { %w[pull] } + access_token(names_and_actions) end def self.pull_nested_repositories_access_token(name) - name = name.chomp('/') if name.end_with?('/') - paths = [name, "#{name}/*"] - access_token(['pull'], paths) + name = name.chomp('/') + + access_token({ + name => %w[pull], + "#{name}/*" => %w[pull] + }) + end + + def self.push_pull_nested_repositories_access_token(name) + name = name.chomp('/') + + access_token({ + name => %w[pull push], + "#{name}/*" => %w[pull] + }) end - def self.access_token(actions, names, type = 'repository') - names = names.flatten + def self.access_token(names_and_actions, type = 'repository') registry = Gitlab.config.registry token = JSONWebToken::RSAToken.new(registry.key) token.issuer = registry.issuer token.audience = AUDIENCE token.expire_time = token_expire_at - token[:access] = names.map do |name| + token[:access] = names_and_actions.map do |name, actions| { type: type, name: name, @@ -219,7 +232,6 @@ module Auth # Overridden in EE def can_access?(requested_project, requested_action) return false unless requested_project.container_registry_enabled? - return false if requested_project.repository_access_level == ::ProjectFeature::DISABLED case requested_action when 'pull' diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index 77ed0369624..d0fde43138a 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -61,9 +61,9 @@ module AutoMerge merge_request.can_be_merged_by?(current_user) && merge_request.open? && !merge_request.broken? && - !merge_request.draft? && - merge_request.mergeable_discussions_state? && - !merge_request.merge_blocked_by_other_mrs? && + (skip_draft_check(merge_request) || !merge_request.draft?) && + (skip_discussions_check(merge_request) || merge_request.mergeable_discussions_state?) && + (skip_blocked_check(merge_request) || !merge_request.merge_blocked_by_other_mrs?) && yield end end @@ -109,5 +109,20 @@ module AutoMerge def track_exception(error, merge_request) Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id) end + + # Will skip the draft check or not when checking if strategy is available + def skip_draft_check(merge_request) + false + end + + # Will skip the blocked check or not when checking if strategy is available + def skip_blocked_check(merge_request) + false + end + + # Will skip the discussions check or not when checking if strategy is available + def skip_discussions_check(merge_request) + false + end end end diff --git a/app/services/branches/delete_service.rb b/app/services/branches/delete_service.rb index 6efbdd161a1..e396d784ca6 100644 --- a/app/services/branches/delete_service.rb +++ b/app/services/branches/delete_service.rb @@ -37,3 +37,5 @@ module Branches end end end + +Branches::DeleteService.prepend_mod diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb index 8fbb7f4f347..70c77444f13 100644 --- a/app/services/bulk_create_integration_service.rb +++ b/app/services/bulk_create_integration_service.rb @@ -10,10 +10,10 @@ class BulkCreateIntegrationService end def execute - service_list = ServiceList.new(batch, integration_hash(:create), association).to_array + integration_list = Integrations::IntegrationList.new(batch, integration_hash(:create), association).to_array Integration.transaction do - results = bulk_insert(*service_list) + results = bulk_insert(*integration_list) if integration.data_fields_present? data_list = DataList.new(results, data_fields_hash(:create), integration.data_fields.class).to_array diff --git a/app/services/bulk_imports/process_service.rb b/app/services/bulk_imports/process_service.rb new file mode 100644 index 00000000000..14c5545cfd5 --- /dev/null +++ b/app/services/bulk_imports/process_service.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +module BulkImports + class ProcessService + PERFORM_DELAY = 5.seconds + DEFAULT_BATCH_SIZE = 5 + + attr_reader :bulk_import + + def initialize(bulk_import) + @bulk_import = bulk_import + end + + def execute + return unless bulk_import + return if bulk_import.completed? + return bulk_import.fail_op! if all_entities_failed? + return bulk_import.finish! if all_entities_processed? && bulk_import.started? + return re_enqueue if max_batch_size_exceeded? # Do not start more jobs if max allowed are already running + + process_bulk_import + re_enqueue + rescue StandardError => e + Gitlab::ErrorTracking.track_exception(e, bulk_import_id: bulk_import.id) + + bulk_import.fail_op + end + + private + + def process_bulk_import + bulk_import.start! if bulk_import.created? + + created_entities.first(next_batch_size).each do |entity| + create_tracker(entity) + + entity.start! + + BulkImports::ExportRequestWorker.perform_async(entity.id) + end + end + + def entities + @entities ||= bulk_import.entities + end + + def created_entities + entities.with_status(:created) + end + + def all_entities_processed? + entities.all? { |entity| entity.finished? || entity.failed? } + end + + def all_entities_failed? + entities.all?(&:failed?) + end + + # A new BulkImportWorker job is enqueued to either + # - Process the new BulkImports::Entity created during import (e.g. for the subgroups) + # - Or to mark the `bulk_import` as finished + def re_enqueue + BulkImportWorker.perform_in(PERFORM_DELAY, bulk_import.id) + end + + def started_entities + entities.with_status(:started) + end + + def max_batch_size_exceeded? + started_entities.count >= DEFAULT_BATCH_SIZE + end + + def next_batch_size + [DEFAULT_BATCH_SIZE - started_entities.count, 0].max + end + + def create_tracker(entity) + entity.class.transaction do + entity.pipelines.each do |pipeline| + status = skip_pipeline?(pipeline, entity) ? :skipped : :created + + entity.trackers.create!( + stage: pipeline[:stage], + pipeline_name: pipeline[:pipeline], + status: BulkImports::Tracker.state_machine.states[status].value + ) + end + end + end + + def skip_pipeline?(pipeline, entity) + return false unless entity.source_version.valid? + + minimum_version, maximum_version = pipeline.values_at(:minimum_source_version, :maximum_source_version) + + if source_version_out_of_range?(minimum_version, maximum_version, entity.source_version.without_patch) + log_skipped_pipeline(pipeline, entity, minimum_version, maximum_version) + return true + end + + false + end + + def source_version_out_of_range?(minimum_version, maximum_version, non_patch_source_version) + (minimum_version && non_patch_source_version < Gitlab::VersionInfo.parse(minimum_version)) || + (maximum_version && non_patch_source_version > Gitlab::VersionInfo.parse(maximum_version)) + end + + def log_skipped_pipeline(pipeline, entity, minimum_version, maximum_version) + logger.info( + message: 'Pipeline skipped as source instance version not compatible with pipeline', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + pipeline_name: pipeline[:pipeline], + minimum_source_version: minimum_version, + maximum_source_version: maximum_version, + source_version: entity.source_version.to_s, + importer: 'gitlab_migration' + ) + end + + def logger + @logger ||= Gitlab::Import::Logger.build + end + end +end diff --git a/app/services/bulk_imports/relation_batch_export_service.rb b/app/services/bulk_imports/relation_batch_export_service.rb index 19eb550216d..c7164d7c304 100644 --- a/app/services/bulk_imports/relation_batch_export_service.rb +++ b/app/services/bulk_imports/relation_batch_export_service.rb @@ -14,6 +14,7 @@ module BulkImports start_batch! export_service.export_batch(relation_batch_ids) + ensure_export_file_exists! compress_exported_relation upload_compressed_file @@ -76,5 +77,15 @@ module BulkImports batch.update!(status_event: 'fail_op', error: exception.message.truncate(255)) end + + def exported_filepath + File.join(export_path, exported_filename) + end + + # Create empty file on disk + # if relation is empty and nothing was exported + def ensure_export_file_exists! + FileUtils.touch(exported_filepath) + end end end diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb index ed71c09420b..91640496440 100644 --- a/app/services/bulk_imports/relation_export_service.rb +++ b/app/services/bulk_imports/relation_export_service.rb @@ -18,6 +18,7 @@ module BulkImports find_or_create_export! do |export| export.remove_existing_upload! export_service.execute + ensure_export_file_exists! compress_exported_relation upload_compressed_file(export) end @@ -91,5 +92,15 @@ module BulkImports export&.update(status_event: 'fail_op', error: exception.class, batched: false) end + + def exported_filepath + File.join(export_path, export_service.exported_filename) + end + + # Create empty file on disk + # if relation is empty and nothing was exported + def ensure_export_file_exists! + FileUtils.touch(exported_filepath) + end end end diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb index 3b204d51bab..a7dc6a47a6b 100644 --- a/app/services/chat_names/find_user_service.rb +++ b/app/services/chat_names/find_user_service.rb @@ -11,7 +11,7 @@ module ChatNames chat_name = find_chat_name return unless chat_name - chat_name.update_last_used_at + record_chat_activity(chat_name) chat_name end @@ -27,5 +27,10 @@ module ChatNames ) end # rubocop: enable CodeReuse/ActiveRecord + + def record_chat_activity(chat_name) + chat_name.update_last_used_at + Users::ActivityService.new(author: chat_name.user).execute + end end end diff --git a/app/services/ci/catalog/resources/validate_service.rb b/app/services/ci/catalog/resources/validate_service.rb new file mode 100644 index 00000000000..9e8986ba6fc --- /dev/null +++ b/app/services/ci/catalog/resources/validate_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Ci + module Catalog + module Resources + class ValidateService + attr_reader :project + + def initialize(project, ref) + @project = project + @ref = ref + @errors = [] + end + + def execute + check_project_readme + check_project_description + + if errors.empty? + ServiceResponse.success + else + ServiceResponse.error(message: errors.join(' , ')) + end + end + + private + + attr_reader :ref, :errors + + def check_project_description + return if project.description.present? + + errors << 'Project must have a description' + end + + def check_project_readme + return if project_has_readme? + + errors << 'Project must have a README' + end + + def project_has_readme? + project.repository.blob_data_at(ref, 'README.md') + end + end + end + end +end diff --git a/app/services/ci/catalog/validate_resource_service.rb b/app/services/ci/catalog/validate_resource_service.rb deleted file mode 100644 index f166c220869..00000000000 --- a/app/services/ci/catalog/validate_resource_service.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Ci - module Catalog - class ValidateResourceService - attr_reader :project - - def initialize(project, ref) - @project = project - @ref = ref - @errors = [] - end - - def execute - check_project_readme - check_project_description - - if errors.empty? - ServiceResponse.success - else - ServiceResponse.error(message: errors.join(' , ')) - end - end - - private - - attr_reader :ref, :errors - - def check_project_description - return if project.description.present? - - errors << 'Project must have a description' - end - - def check_project_readme - return if project_has_readme? - - errors << 'Project must have a README' - end - - def project_has_readme? - project.repository.blob_data_at(ref, 'README.md') - end - end - end -end diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb index 45abb415174..4f09d47b530 100644 --- a/app/services/ci/components/fetch_service.rb +++ b/app/services/ci/components/fetch_service.rb @@ -5,8 +5,6 @@ module Ci class FetchService include Gitlab::Utils::StrongMemoize - TEMPLATE_FILE = 'template.yml' - COMPONENT_PATHS = [ ::Gitlab::Ci::Components::InstancePath ].freeze @@ -23,11 +21,16 @@ module Ci reason: :unsupported_path) end - component_path = component_path_class.new(address: address, content_filename: TEMPLATE_FILE) - content = component_path.fetch_content!(current_user: current_user) + component_path = component_path_class.new(address: address) + result = component_path.fetch_content!(current_user: current_user) - if content.present? - ServiceResponse.success(payload: { content: content, path: component_path }) + if result + ServiceResponse.success(payload: { + content: result.content, + path: result.path, + project: component_path.project, + sha: component_path.sha + }) else ServiceResponse.error(message: "#{error_prefix} content not found", reason: :content_not_found) end diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb index 57b95e59d7d..4d688d79982 100644 --- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb +++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb @@ -44,10 +44,6 @@ module Ci def destroy_batch(artifacts) Ci::JobArtifacts::DestroyBatchService.new(artifacts, skip_projects_on_refresh: true).execute end - - def loop_timeout? - Time.current > @start_at + LOOP_TIMEOUT - end end end end diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb index 05cd20a152b..c18984953a1 100644 --- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb +++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb @@ -19,20 +19,11 @@ module Ci return if pipeline.parent_pipeline? # skip if child pipeline return unless project.auto_cancel_pending_pipelines? - if Feature.enabled?(:use_offset_pagination_for_canceling_redundant_pipelines, project) - paginator.each do |ids| - pipelines = parent_and_child_pipelines(ids) + paginator.each do |ids| + pipelines = parent_and_child_pipelines(ids) - Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables| - auto_cancel_interruptible_pipelines(cancelables.ids) - end - end - else - Gitlab::OptimisticLocking - .retry_lock(parent_and_child_pipelines, name: 'cancel_pending_pipelines') do |cancelables| - cancelables.select(:id).each_batch(of: BATCH_SIZE) do |cancelables_batch| - auto_cancel_interruptible_pipelines(cancelables_batch.ids) - end + Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables| + auto_cancel_interruptible_pipelines(cancelables.ids) end end end @@ -61,7 +52,7 @@ module Ci end end - def parent_auto_cancelable_pipelines(ids = nil) + def parent_auto_cancelable_pipelines(ids) scope = project.all_pipelines .created_after(pipelines_created_after) .for_ref(pipeline.ref) @@ -70,11 +61,10 @@ module Ci .for_status(CommitStatus::AVAILABLE_STATUSES) # Force usage of project_id_and_status_and_created_at_index .ci_sources - scope = scope.id_in(ids) if ids.present? - scope + scope.id_in(ids) end - def parent_and_child_pipelines(ids = nil) + def parent_and_child_pipelines(ids) Ci::Pipeline.object_hierarchy(parent_auto_cancelable_pipelines(ids), project_condition: :same) .base_and_descendants .alive_or_scheduled diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 750272c3ecb..84e5089b0d5 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -93,6 +93,8 @@ module Ci # We do not continue to process the job if the previous status is not completed return unless Ci::HasStatus::COMPLETED_STATUSES.include?(previous_status) + ::Deployments::CreateForJobService.new.execute(job) + Gitlab::OptimisticLocking.retry_lock(job, name: 'atomic_processing_update_job') do |subject| Ci::ProcessBuildService.new(project, subject.user) .execute(subject, previous_status) diff --git a/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb new file mode 100644 index 00000000000..319186ce030 --- /dev/null +++ b/app/services/ci/refs/enqueue_pipelines_to_unlock_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Ci + module Refs + class EnqueuePipelinesToUnlockService + include BaseServiceUtility + + BATCH_SIZE = 50 + ENQUEUE_INTERVAL_SECONDS = 0.1 + + def execute(ci_ref, before_pipeline: nil) + pipelines_scope = ci_ref.pipelines.artifacts_locked + pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline + total_new_entries = 0 + + pipelines_scope.each_batch(of: BATCH_SIZE) do |batch| + pipeline_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord + total_added = Ci::UnlockPipelineRequest.enqueue(pipeline_ids) + total_new_entries += total_added + + # Take a little rest to avoid overloading Redis + sleep ENQUEUE_INTERVAL_SECONDS + end + + success( + total_pending_entries: Ci::UnlockPipelineRequest.total_pending, + total_new_entries: total_new_entries + ) + end + end + end +end diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb index 14ea09f17a0..d7c3e9e7f64 100644 --- a/app/services/ci/retry_job_service.rb +++ b/app/services/ci/retry_job_service.rb @@ -39,7 +39,9 @@ module Ci ::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job) - ::Deployments::CreateForJobService.new.execute(new_job) + if Feature.disabled?(:create_deployment_only_for_processable_jobs, project) + ::Deployments::CreateForJobService.new.execute(new_job) + end ::MergeRequests::AddTodoWhenBuildFailsService .new(project: project) diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 85f910d05d7..f6b2c90c6ec 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -26,9 +26,7 @@ module Ci .new(project: project, current_user: current_user) .close_all(pipeline) - Ci::ProcessPipelineService - .new(pipeline) - .execute + start_pipeline(pipeline) ServiceResponse.success rescue Gitlab::Access::AccessDeniedError => e @@ -52,6 +50,10 @@ module Ci def can_be_retried?(build) can?(current_user, :update_build, build) end + + def start_pipeline(pipeline) + Ci::PipelineCreation::StartPipelineService.new(pipeline).execute + end end end diff --git a/app/services/ci/unlock_pipeline_service.rb b/app/services/ci/unlock_pipeline_service.rb new file mode 100644 index 00000000000..88d4a8fd0be --- /dev/null +++ b/app/services/ci/unlock_pipeline_service.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module Ci + class UnlockPipelineService + include BaseServiceUtility + include ::Gitlab::ExclusiveLeaseHelpers + + ExecutionTimeoutError = Class.new(StandardError) + + BATCH_SIZE = 100 + MAX_EXEC_DURATION = 10.minutes.freeze + LOCK_TIMEOUT = (MAX_EXEC_DURATION + 1.minute).freeze + + def initialize(pipeline) + @pipeline = pipeline + @already_leased = false + @already_unlocked = false + @exec_timeout = false + @unlocked_job_artifacts_count = 0 + @unlocked_pipeline_artifacts_count = 0 + end + + def execute + unlock_pipeline_exclusively + + success( + skipped_already_leased: already_leased, + skipped_already_unlocked: already_unlocked, + exec_timeout: exec_timeout, + unlocked_job_artifacts: unlocked_job_artifacts_count, + unlocked_pipeline_artifacts: unlocked_pipeline_artifacts_count + ) + end + + private + + attr_reader :pipeline, :already_leased, :already_unlocked, :exec_timeout, + :unlocked_job_artifacts_count, :unlocked_pipeline_artifacts_count + + def unlock_pipeline_exclusively + in_lock(lock_key, ttl: LOCK_TIMEOUT, retries: 0) do + # Even though we enforce uniqueness when enqueueing pipelines, there is still a rare race condition chance that + # a pipeline can be re-enqueued right after a worker pops off the same pipeline ID from the queue, and then just + # after it completing the unlock process and releasing the lock, another worker picks up the re-enqueued + # pipeline ID. So let's make sure to only unlock artifacts if the pipeline has not been unlocked. + if pipeline.unlocked? + @already_unlocked = true + break + end + + unlock_job_artifacts + unlock_pipeline_artifacts + + # Marking the row in `ci_pipelines` to unlocked signifies that all artifacts have + # already been unlocked. This must always happen last. + unlock_pipeline + end + rescue ExecutionTimeoutError + @exec_timeout = true + rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + @already_leased = true + ensure + if pipeline.unlocked? + Ci::UnlockPipelineRequest.log_event(:completed, pipeline.id) unless already_unlocked + else + # This is to ensure to re-enqueue the pipeline in 2 occasions: + # 1. When an unexpected error happens. + # 2. When the execution timeout has been reached in the case of a pipeline having a lot of + # job artifacts. This allows us to continue unlocking the rest of the artifacts from + # where we left off. This is why we unlock the pipeline last. + Ci::UnlockPipelineRequest.enqueue(pipeline.id) + Ci::UnlockPipelineRequest.log_event(:re_enqueued, pipeline.id) + end + end + + def lock_key + "ci:unlock_pipeline_service:lock:#{pipeline.id}" + end + + def unlock_pipeline + pipeline.update_column(:locked, Ci::Pipeline.lockeds[:unlocked]) + end + + def unlock_job_artifacts + start = Time.current + + pipeline.builds.each_batch(of: BATCH_SIZE) do |builds| + # rubocop: disable CodeReuse/ActiveRecord + Ci::JobArtifact.where(job_id: builds.pluck(:id)).each_batch(of: BATCH_SIZE) do |job_artifacts| + unlocked_count = Ci::JobArtifact + .where(id: job_artifacts.pluck(:id)) + .update_all(locked: :unlocked) + + @unlocked_job_artifacts_count ||= 0 + @unlocked_job_artifacts_count += unlocked_count + + raise ExecutionTimeoutError if (Time.current - start) > MAX_EXEC_DURATION + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + + def unlock_pipeline_artifacts + @unlocked_pipeline_artifacts_count = pipeline.pipeline_artifacts.update_all(locked: :unlocked) + end + end +end diff --git a/app/services/clusters/agent_tokens/revoke_service.rb b/app/services/clusters/agent_tokens/revoke_service.rb index 5d89b405969..46873fbbc47 100644 --- a/app/services/clusters/agent_tokens/revoke_service.rb +++ b/app/services/clusters/agent_tokens/revoke_service.rb @@ -13,7 +13,7 @@ module Clusters def execute return error_no_permissions unless current_user.can?(:create_cluster, token.agent.project) - if token.update(status: token.class.statuses[:revoked]) + if token.revoke! log_activity_event(token) ServiceResponse.success diff --git a/app/services/clusters/cleanup/project_namespace_service.rb b/app/services/clusters/cleanup/project_namespace_service.rb index f6ac06d0594..3d5a4f85d10 100644 --- a/app/services/clusters/cleanup/project_namespace_service.rb +++ b/app/services/clusters/cleanup/project_namespace_service.rb @@ -40,7 +40,7 @@ module Clusters cluster.kubeclient&.delete_namespace(kubernetes_namespace.namespace) rescue Kubeclient::ResourceNotFoundError # The resources have already been deleted, possibly on a previous attempt that timed out - rescue Gitlab::UrlBlocker::BlockedUrlError + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError # User gave an invalid cluster from the start, or deleted the endpoint before this job ran end end diff --git a/app/services/clusters/cleanup/service_account_service.rb b/app/services/clusters/cleanup/service_account_service.rb index 0ce4bf9bb9c..0358a5412b3 100644 --- a/app/services/clusters/cleanup/service_account_service.rb +++ b/app/services/clusters/cleanup/service_account_service.rb @@ -22,7 +22,7 @@ module Clusters ) rescue Kubeclient::ResourceNotFoundError # The resources have already been deleted, possibly on a previous attempt that timed out - rescue Gitlab::UrlBlocker::BlockedUrlError + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError # User gave an invalid cluster from the start, or deleted the endpoint before this job ran rescue Kubeclient::HttpError => e # unauthorized, forbidden: GitLab's access has been revoked diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index 89370bd8abb..5fc84e5aad7 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -40,11 +40,7 @@ module Commits Gitlab::Git::CommandError => ex Gitlab::ErrorTracking.log_exception(ex) - if Feature.enabled?(:errors_utf_8_encoding) - error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message)) - else - error(ex.message) - end + error(Gitlab::EncodingHelper.encode_utf8_no_detect(ex.message)) end private diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb index f14c79ecd7e..50963cc58b2 100644 --- a/app/services/concerns/update_repository_storage_methods.rb +++ b/app/services/concerns/update_repository_storage_methods.rb @@ -82,7 +82,6 @@ module UpdateRepositoryStorageMethods repository = type.repository_for(type.design? ? container.design_management_repository : container) full_path = repository.full_path raw_repository = repository.raw - checksum = repository.checksum # Initialize a git repository on the target path new_repository = Gitlab::Git::Repository.new( @@ -92,12 +91,7 @@ module UpdateRepositoryStorageMethods full_path ) - new_repository.replicate(raw_repository) - new_checksum = new_repository.checksum - - if checksum != new_checksum - raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum } - end + Repositories::ReplicateService.new(raw_repository).execute(new_repository, type.name) end def same_filesystem? diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb index 1a03b444b68..a54c4947b0b 100644 --- a/app/services/concerns/users/participable_service.rb +++ b/app/services/concerns/users/participable_service.rb @@ -34,7 +34,7 @@ module Users def groups return [] unless current_user - current_user.authorized_groups.with_route.sort_by(&:path) + current_user.authorized_groups.with_route.sort_by(&:full_path) end def render_participants_as_hash(participants) diff --git a/app/services/deployments/create_for_job_service.rb b/app/services/deployments/create_for_job_service.rb index e230515ce27..fb07efe8694 100644 --- a/app/services/deployments/create_for_job_service.rb +++ b/app/services/deployments/create_for_job_service.rb @@ -38,8 +38,6 @@ module Deployments return unless deployment.valid? && deployment.environment.persisted? if cluster = deployment.environment.deployment_platform&.cluster # rubocop: disable Lint/AssignmentInCondition - # double write cluster_id until 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628 - deployment.cluster_id = cluster.id deployment.deployment_cluster = ::DeploymentCluster.new( cluster_id: cluster.id, kubernetes_namespace: cluster.kubernetes_namespace_for(deployment.environment, deployable: job) diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb index ebf2b077bca..8ef9982f41b 100644 --- a/app/services/deployments/create_service.rb +++ b/app/services/deployments/create_service.rb @@ -28,7 +28,6 @@ module Deployments # We use explicit parameters here so we never by accident allow parameters # to be set that one should not be able to set (e.g. the row ID). { - cluster_id: environment.deployment_platform&.cluster_id, project_id: environment.project_id, environment_id: environment.id, ref: params[:ref], diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 31da099d078..a2eb4f1f396 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -156,7 +156,7 @@ module Git return if branch_to_sync.nil? && commits_to_sync.empty? - if commits_to_sync.any? && Feature.enabled?(:batch_delay_jira_branch_sync_worker, project) + if commits_to_sync.any? commits_to_sync.each_slice(JIRA_SYNC_BATCH_SIZE).with_index do |commits, i| JiraConnect::SyncBranchWorker.perform_in( JIRA_SYNC_BATCH_DELAY * i, diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb index 3d961780889..e628e88eaa9 100644 --- a/app/services/import/bitbucket_server_service.rb +++ b/app/services/import/bitbucket_server_service.rb @@ -42,7 +42,8 @@ module Import project_name, target_namespace, current_user, - credentials + credentials, + timeout_strategy ).execute end @@ -74,6 +75,10 @@ module Import @url ||= params[:bitbucket_server_url] end + def timeout_strategy + @timeout_strategy ||= params[:timeout_strategy] || ProjectImportData::PESSIMISTIC_TIMEOUT + end + def allow_local_requests? Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb index 73e0c229a9c..86c62145a87 100644 --- a/app/services/import/github_service.rb +++ b/app/services/import/github_service.rb @@ -138,6 +138,7 @@ module Import Gitlab::GithubImport::Settings .new(project) .write( + timeout_strategy: params[:timeout_strategy] || ProjectImportData::PESSIMISTIC_TIMEOUT, optional_stages: params[:optional_stages], additional_access_tokens: access_params[:additional_access_tokens] ) diff --git a/app/services/import/validate_remote_git_endpoint_service.rb b/app/services/import/validate_remote_git_endpoint_service.rb index a994072c4aa..2177238fddf 100644 --- a/app/services/import/validate_remote_git_endpoint_service.rb +++ b/app/services/import/validate_remote_git_endpoint_service.rb @@ -13,6 +13,8 @@ module Import GIT_PROTOCOL_PKT_LEN = 4 GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement" + INVALID_BODY_MESSAGE = 'Not a git repository: Invalid response body' + INVALID_CONTENT_TYPE_MESSAGE = 'Not a git repository: Invalid content-type' def initialize(params) @params = params @@ -30,32 +32,35 @@ module Import uri.fragment = nil url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}") - response_body = '' - result = nil - Gitlab::HTTP.try_get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |fragment| - response_body += fragment - next if response_body.length < GIT_MINIMUM_RESPONSE_LENGTH - - result = if status_code_is_valid(fragment) && content_type_is_valid(fragment) && response_body_is_valid(response_body) - :success - else - :error - end - - # We are interested only in the first chunks of the response - # So we're using stream_body: true and breaking when receive enough body - break - end + response, response_body = http_get_and_extract_first_chunks(url) - if result == :success - ServiceResponse.success - else - ServiceResponse.error(message: "#{uri} is not a valid HTTP Git repository") - end + validate(uri, response, response_body) + rescue *Gitlab::HTTP::HTTP_ERRORS => err + error_result("HTTP #{err.class.name.underscore} error: #{err.message}") + rescue StandardError => err + ServiceResponse.error( + message: "Internal #{err.class.name.underscore} error: #{err.message}", + reason: 500 + ) end private + def http_get_and_extract_first_chunks(url) + # We are interested only in the first chunks of the response + # So we're using stream_body: true and breaking when receive enough body + response = nil + response_body = '' + + Gitlab::HTTP.get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |response_chunk| + response = response_chunk + response_body += response_chunk + break if GIT_MINIMUM_RESPONSE_LENGTH <= response_body.length + end + + [response, response_body] + end + def auth unless @params[:user].to_s.blank? { @@ -65,16 +70,38 @@ module Import end end - def status_code_is_valid(fragment) - fragment.http_response.code == '200' + def validate(uri, response, response_body) + return status_code_error(uri, response) unless status_code_is_valid?(response) + return error_result(INVALID_CONTENT_TYPE_MESSAGE) unless content_type_is_valid?(response) + return error_result(INVALID_BODY_MESSAGE) unless response_body_is_valid?(response_body) + + ServiceResponse.success + end + + def status_code_error(uri, response) + http_code = response.http_response.code.to_i + message = response.http_response.message || Rack::Utils::HTTP_STATUS_CODES[http_code] + + error_result( + "#{uri} endpoint error: #{http_code}#{message.presence&.prepend(' ')}", + http_code + ) + end + + def error_result(message, reason = nil) + ServiceResponse.error(message: message, reason: reason) + end + + def status_code_is_valid?(response) + response.http_response.code == '200' end - def content_type_is_valid(fragment) - fragment.http_response['content-type'] == EXPECTED_CONTENT_TYPE + def content_type_is_valid?(response) + response.http_response['content-type'] == EXPECTED_CONTENT_TYPE end - def response_body_is_valid(response_body) - response_body.match?(GIT_BODY_MESSAGE_REGEXP) + def response_body_is_valid?(response_body) + response_body.length <= GIT_MINIMUM_RESPONSE_LENGTH && response_body.match?(GIT_BODY_MESSAGE_REGEXP) end end end diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb index a4e815e70fc..d7fdd235db1 100644 --- a/app/services/issuable/clone/base_service.rb +++ b/app/services/issuable/clone/base_service.rb @@ -93,5 +93,3 @@ module Issuable end end end - -Issuable::Clone::BaseService.prepend_mod_with('Issuable::Clone::BaseService') diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb index 761ba3f74aa..c855a58522c 100644 --- a/app/services/issuable_links/create_service.rb +++ b/app/services/issuable_links/create_service.rb @@ -2,13 +2,14 @@ module IssuableLinks class CreateService < BaseService - attr_reader :issuable, :current_user, :params + attr_reader :issuable, :current_user, :params, :new_links def initialize(issuable, user, params) @issuable = issuable @current_user = user @params = params.dup @errors = [] + @new_links = [] end def execute @@ -45,6 +46,7 @@ module IssuableLinks set_link_type(link) if link.changed? && link.save + new_links << link create_notes(link) end diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb index db05920678e..3523e945d37 100644 --- a/app/services/issue_links/create_service.rb +++ b/app/services/issue_links/create_service.rb @@ -9,7 +9,7 @@ module IssueLinks end def previous_related_issuables - @related_issues ||= issuable.related_issues(current_user).to_a + @related_issues ||= issuable.related_issues(authorize: false).to_a end private diff --git a/app/services/issues/set_crm_contacts_service.rb b/app/services/issues/set_crm_contacts_service.rb index 5836097f1fd..c2ed7c554be 100644 --- a/app/services/issues/set_crm_contacts_service.rb +++ b/app/services/issues/set_crm_contacts_service.rb @@ -13,7 +13,7 @@ module Issues return error_invalid_params unless valid_params? @existing_ids = issue.customer_relations_contact_ids - determine_changes if params[:replace_ids].present? + determine_changes if set_present? return error_too_many if too_many? @added_count = 0 @@ -108,7 +108,7 @@ module Issues end def set_present? - params[:replace_ids].present? + !params[:replace_ids].nil? end def add_or_remove_present? diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb index 497c282072d..5d9292a6967 100644 --- a/app/services/jira_connect/sync_service.rb +++ b/app/services/jira_connect/sync_service.rb @@ -9,6 +9,8 @@ module JiraConnect # Parameters: see Atlassian::JiraConnect::Client#send_info # Includes: update_sequence_id, commits, branches, merge_requests, pipelines def execute(**args) + preload_reviewers_for_merge_requests(args[:merge_requests]) if args.key?(:merge_requests) + JiraConnectInstallation.for_project(project).flat_map do |installation| client = Atlassian::JiraConnect::Client.new(installation.base_url, installation.shared_secret) @@ -43,5 +45,11 @@ module JiraConnect def logger Gitlab::IntegrationsLogger end + + def preload_reviewers_for_merge_requests(merge_requests) + ActiveRecord::Associations::Preloader.new( + records: merge_requests, associations: [:approvals, { merge_request_reviewers: :reviewer }] + ).call + end end end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index aba075c3644..9cedc7ee3a5 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -16,7 +16,6 @@ module Members @errors = [] @invites = invites_from_params @source = params[:source] - @tasks_to_be_done_members = [] end def execute @@ -31,13 +30,13 @@ module Members validate_invitable! add_members - create_tasks_to_be_done enqueue_onboarding_progress_action publish_event! result rescue BlankInvitesError, TooManyInvitesError, MembershipLockedError => e + Gitlab::ErrorTracking.log_exception(e, class: self.class.to_s, user_id: current_user.id) error(e.message) end @@ -47,8 +46,7 @@ module Members private - attr_reader :source, :errors, :invites, :member_created_namespace_id, :members, - :tasks_to_be_done_members, :member_created_member_task_id + attr_reader :source, :errors, :invites, :member_created_namespace_id, :members def adding_at_least_one_owner params[:access_level] == Gitlab::Access::OWNER @@ -88,9 +86,7 @@ module Members invites, params[:access_level], expires_at: params[:expires_at], - current_user: current_user, - tasks_to_be_done: params[:tasks_to_be_done], - tasks_project_id: params[:tasks_project_id] + current_user: current_user ) members.each { |member| process_result(member) } @@ -123,7 +119,6 @@ module Members def after_execute(member:) super - build_tasks_to_be_done_members(member) track_invite_source(member) end @@ -146,30 +141,6 @@ module Members member.invite? ? 'net_new_user' : 'existing_user' end - def build_tasks_to_be_done_members(member) - return unless tasks_to_be_done?(member) - - @tasks_to_be_done_members << member - # We can take the first `member_task` here, since all tasks will have the same attributes needed - # for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`. - @member_created_member_task_id ||= member.member_task.id - end - - def tasks_to_be_done?(member) - return false if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank? - - # Only create task issues for existing users. Tasks for new users are created when they signup. - member.member_task&.valid? && member.user.present? - end - - def create_tasks_to_be_done - return unless member_created_member_task_id # signal if there is any work to be done here - - TasksToBeDone::CreateWorker.perform_async(member_created_member_task_id, - current_user.id, - tasks_to_be_done_members.map(&:user_id)) - end - def user_limit limit = params.fetch(:limit, DEFAULT_INVITE_LIMIT) diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb index cc18aae7446..22d8b30db18 100644 --- a/app/services/members/creator_service.rb +++ b/app/services/members/creator_service.rb @@ -53,8 +53,7 @@ module Members common_arguments = { source: source, access_level: access_level, - existing_members: existing_members, - tasks_to_be_done: args[:tasks_to_be_done] || [] + existing_members: existing_members }.merge(parsed_args(args)) members = emails.map do |email| @@ -81,7 +80,6 @@ module Members { current_user: args[:current_user], expires_at: args[:expires_at], - tasks_project_id: args[:tasks_project_id], ldap: args[:ldap] } end @@ -212,22 +210,7 @@ module Members end def after_commit_tasks - create_member_task - end - - def create_member_task - return unless member.persisted? - return if member_task_attributes.value?(nil) - return if member.member_task.present? - - member.create_member_task(member_task_attributes) - end - - def member_task_attributes - { - tasks_to_be_done: args[:tasks_to_be_done], - project_id: args[:tasks_project_id] - } + # hook for overriding in other uses end def approve_request diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb index dbe5567cbc5..f9857cdad39 100644 --- a/app/services/merge_requests/approval_service.rb +++ b/app/services/merge_requests/approval_service.rb @@ -7,7 +7,7 @@ module MergeRequests approval = merge_request.approvals.new( user: current_user, - patch_id_sha: fetch_patch_id_sha(merge_request) + patch_id_sha: merge_request.current_patch_id_sha ) return success unless save_approval(approval) @@ -36,17 +36,6 @@ module MergeRequests private - def fetch_patch_id_sha(merge_request) - diff_refs = merge_request.diff_refs - base_sha = diff_refs&.base_sha - head_sha = diff_refs&.head_sha - - return unless base_sha && head_sha - return if base_sha == head_sha - - merge_request.project.repository.get_patch_id(base_sha, head_sha) - end - def eligible_for_approval?(merge_request) merge_request.eligible_for_approval_by?(current_user) end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 29aba3c8679..89e5920a4fb 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -181,3 +181,5 @@ module MergeRequests end end end + +MergeRequests::MergeService.prepend_mod diff --git a/app/services/merge_requests/mergeability/check_base_service.rb b/app/services/merge_requests/mergeability/check_base_service.rb index e614a7c27fe..e1c4d751296 100644 --- a/app/services/merge_requests/mergeability/check_base_service.rb +++ b/app/services/merge_requests/mergeability/check_base_service.rb @@ -9,6 +9,10 @@ module MergeRequests @params = params end + def self.identifier + failure_reason + end + def skip? raise NotImplementedError end @@ -24,12 +28,22 @@ module MergeRequests private + def failure_reason + self.class.failure_reason + end + def success(**args) - Gitlab::MergeRequests::Mergeability::CheckResult.success(payload: args) + Gitlab::MergeRequests::Mergeability::CheckResult + .success(payload: default_payload(args)) end def failure(**args) - Gitlab::MergeRequests::Mergeability::CheckResult.failed(payload: args) + Gitlab::MergeRequests::Mergeability::CheckResult + .failed(payload: default_payload(args)) + end + + def default_payload(args) + args.merge(identifier: self.class.identifier) end end end diff --git a/app/services/merge_requests/mergeability/check_broken_status_service.rb b/app/services/merge_requests/mergeability/check_broken_status_service.rb index 6fe4eb4a57f..25293c53bb5 100644 --- a/app/services/merge_requests/mergeability/check_broken_status_service.rb +++ b/app/services/merge_requests/mergeability/check_broken_status_service.rb @@ -2,6 +2,10 @@ module MergeRequests module Mergeability class CheckBrokenStatusService < CheckBaseService + def self.failure_reason + :broken_status + end + def execute if merge_request.broken? failure(reason: failure_reason) @@ -17,12 +21,6 @@ module MergeRequests def cacheable? false end - - private - - def failure_reason - :broken_status - end end end end diff --git a/app/services/merge_requests/mergeability/check_ci_status_service.rb b/app/services/merge_requests/mergeability/check_ci_status_service.rb index 9e09b513c57..f7fa3259d97 100644 --- a/app/services/merge_requests/mergeability/check_ci_status_service.rb +++ b/app/services/merge_requests/mergeability/check_ci_status_service.rb @@ -2,6 +2,10 @@ module MergeRequests module Mergeability class CheckCiStatusService < CheckBaseService + def self.failure_reason + :ci_must_pass + end + def execute if merge_request.mergeable_ci_state? success @@ -17,12 +21,6 @@ module MergeRequests def cacheable? false end - - private - - def failure_reason - :ci_must_pass - end end end end diff --git a/app/services/merge_requests/mergeability/check_conflict_status_service.rb b/app/services/merge_requests/mergeability/check_conflict_status_service.rb new file mode 100644 index 00000000000..2bc253322c9 --- /dev/null +++ b/app/services/merge_requests/mergeability/check_conflict_status_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module MergeRequests + module Mergeability + class CheckConflictStatusService < CheckBaseService + def self.failure_reason + :conflict + end + + def execute + if merge_request.can_be_merged? + success + else + failure(reason: failure_reason) + end + end + + def skip? + false + end + + def cacheable? + false + end + end + end +end diff --git a/app/services/merge_requests/mergeability/check_discussions_status_service.rb b/app/services/merge_requests/mergeability/check_discussions_status_service.rb index 3421d96e8ae..34db5f8a944 100644 --- a/app/services/merge_requests/mergeability/check_discussions_status_service.rb +++ b/app/services/merge_requests/mergeability/check_discussions_status_service.rb @@ -2,6 +2,10 @@ module MergeRequests module Mergeability class CheckDiscussionsStatusService < CheckBaseService + def self.failure_reason + :discussions_not_resolved + end + def execute if merge_request.mergeable_discussions_state? success @@ -17,12 +21,6 @@ module MergeRequests def cacheable? false end - - private - - def failure_reason - :discussions_not_resolved - end end end end diff --git a/app/services/merge_requests/mergeability/check_draft_status_service.rb b/app/services/merge_requests/mergeability/check_draft_status_service.rb index a1524317155..85b67fdc629 100644 --- a/app/services/merge_requests/mergeability/check_draft_status_service.rb +++ b/app/services/merge_requests/mergeability/check_draft_status_service.rb @@ -3,6 +3,10 @@ module MergeRequests module Mergeability class CheckDraftStatusService < CheckBaseService + def self.failure_reason + :draft_status + end + def execute if merge_request.draft? failure(reason: failure_reason) @@ -12,18 +16,12 @@ module MergeRequests end def skip? - false + params[:skip_draft_check].present? end def cacheable? false end - - private - - def failure_reason - :draft_status - end end end end diff --git a/app/services/merge_requests/mergeability/check_open_status_service.rb b/app/services/merge_requests/mergeability/check_open_status_service.rb index 29f3d0d3ccb..f5b70f18394 100644 --- a/app/services/merge_requests/mergeability/check_open_status_service.rb +++ b/app/services/merge_requests/mergeability/check_open_status_service.rb @@ -3,6 +3,10 @@ module MergeRequests module Mergeability class CheckOpenStatusService < CheckBaseService + def self.failure_reason + :not_open + end + def execute if merge_request.open? success @@ -18,12 +22,6 @@ module MergeRequests def cacheable? false end - - private - - def failure_reason - :not_open - end end end end diff --git a/app/services/merge_requests/mergeability/check_rebase_status_service.rb b/app/services/merge_requests/mergeability/check_rebase_status_service.rb new file mode 100644 index 00000000000..2163fec8bd6 --- /dev/null +++ b/app/services/merge_requests/mergeability/check_rebase_status_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module MergeRequests + module Mergeability + class CheckRebaseStatusService < CheckBaseService + def self.failure_reason + :need_rebase + end + + def execute + if merge_request.should_be_rebased? + failure(reason: failure_reason) + else + success + end + end + + def skip? + params[:skip_rebase_check].present? + end + + def cacheable? + false + end + end + end +end diff --git a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb index 987d6ce8e9f..86c8122604c 100644 --- a/app/services/merge_requests/mergeability/detailed_merge_status_service.rb +++ b/app/services/merge_requests/mergeability/detailed_merge_status_service.rb @@ -24,7 +24,7 @@ module MergeRequests ci_check_failure_reason end else - check_results.failure_reason + check_results.payload[:failure_reason] end end @@ -46,7 +46,11 @@ module MergeRequests def check_results strong_memoize(:check_results) do - merge_request.execute_merge_checks(params: { skip_ci_check: true }) + merge_request + .execute_merge_checks( + MergeRequest.mergeable_state_checks, + params: { skip_ci_check: true } + ) end end diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb index 740a6feac2c..5150c03d0a3 100644 --- a/app/services/merge_requests/mergeability/run_checks_service.rb +++ b/app/services/merge_requests/mergeability/run_checks_service.rb @@ -9,8 +9,8 @@ module MergeRequests @params = params end - def execute - @results = merge_request.mergeability_checks.each_with_object([]) do |check_class, result_hash| + def execute(checks, execute_all: false) + @results = checks.each_with_object([]) do |check_class, result_hash| check = check_class.new(merge_request: merge_request, params: params) next if check.skip? @@ -21,24 +21,20 @@ module MergeRequests result_hash << check_result - break result_hash if check_result.failed? + break result_hash if check_result.failed? && !execute_all end logger.commit - self - end - - def success? - raise 'Execute needs to be called before' if results.nil? - - results.all?(&:success?) - end - - def failure_reason - raise 'Execute needs to be called before' if results.nil? + return ServiceResponse.success(payload: { results: results }) if all_results_success? - results.find(&:failed?)&.payload&.fetch(:reason)&.to_sym + ServiceResponse.error( + message: 'Checks failed.', + payload: { + results: results, + failure_reason: failure_reason + } + ) end private @@ -67,6 +63,14 @@ module MergeRequests MergeRequests::Mergeability::Logger.new(merge_request: merge_request) end end + + def all_results_success? + results.all?(&:success?) + end + + def failure_reason + results.find(&:failed?)&.payload&.fetch(:reason)&.to_sym + end end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index c435048e343..37a829e3014 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -185,6 +185,7 @@ module MergeRequests # email template itself, see `change_in_merge_request_draft_status_email` template. notify_draft_status_changed(merge_request) trigger_merge_request_status_updated(merge_request) + publish_draft_change_event(merge_request) if Feature.enabled?(:additional_merge_when_checks_ready, project) end if !old_title_draft && new_title_draft @@ -196,6 +197,14 @@ module MergeRequests end end + def publish_draft_change_event(merge_request) + Gitlab::EventStore.publish( + MergeRequests::DraftStateChangeEvent.new( + data: { current_user_id: current_user.id, merge_request_id: merge_request.id } + ) + ) + end + def notify_draft_status_changed(merge_request) notification_service.async.change_in_merge_request_draft_status( merge_request, diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb index 1316b2546b9..f4d3f3e72d3 100644 --- a/app/services/ml/find_or_create_model_version_service.rb +++ b/app/services/ml/find_or_create_model_version_service.rb @@ -11,7 +11,6 @@ module Ml def execute model = Ml::FindOrCreateModelService.new(project, name).execute - Ml::ModelVersion.find_or_create!(model, version, package) end diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index cba7398ebc0..1b852710677 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -61,7 +61,7 @@ module Notes service_errors = if service_response.respond_to?(:errors) service_response.errors.full_messages elsif service_response.respond_to?(:[]) && service_response[:status] == :error - service_response[:message] + Array.wrap(service_response[:message]) end service_errors.blank? ? ServiceResponse.success : ServiceResponse.error(message: service_errors) diff --git a/app/services/packages/create_dependency_service.rb b/app/services/packages/create_dependency_service.rb index 10a86e44cb0..51f8a514c55 100644 --- a/app/services/packages/create_dependency_service.rb +++ b/app/services/packages/create_dependency_service.rb @@ -59,7 +59,7 @@ module Packages # The bulk_insert statement above do not dirty the query cache. To make # sure that the results are fresh from the database and not from a stalled # and potentially wrong cache, this query has to be done with the query - # chache disabled. + # cache disabled. Packages::Dependency.ids_for_package_names_and_version_patterns(names_and_version_patterns) end end diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb index ac0c77391d7..2ff3ebc3bb2 100644 --- a/app/services/packages/maven/find_or_create_package_service.rb +++ b/app/services/packages/maven/find_or_create_package_service.rb @@ -10,7 +10,7 @@ module Packages package = ::Packages::Maven::PackageFinder.new(current_user, project, path: path) - .execute + .execute&.last unless Namespace::PackageSetting.duplicates_allowed?(package) return ServiceResponse.error(message: 'Duplicate package is not allowed') if target_package_is_duplicate?(package) diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index f6f2dbb8415..d599cecc8da 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -5,7 +5,7 @@ module Packages include Gitlab::Utils::StrongMemoize include ExclusiveLeaseGuard - PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename licenseText].freeze + PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename licenseText contributors exports].freeze DEFAULT_LEASE_TIMEOUT = 1.hour.to_i def execute diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb index cc040a45016..fd4f9b5d1c1 100644 --- a/app/services/packages/nuget/extract_metadata_file_service.rb +++ b/app/services/packages/nuget/extract_metadata_file_service.rb @@ -7,48 +7,30 @@ module Packages MAX_FILE_SIZE = 4.megabytes.freeze - def initialize(package_file) - @package_file = package_file + def initialize(package_zip_file) + @package_zip_file = package_zip_file end def execute - raise ExtractionError, 'invalid package file' unless valid_package_file? - ServiceResponse.success(payload: nuspec_file_content) end private - attr_reader :package_file - - def valid_package_file? - package_file && - package_file.package&.nuget? && - package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate - end + attr_reader :package_zip_file def nuspec_file_content - with_zip_file do |zip_file| - entry = zip_file.glob('*.nuspec').first - - raise ExtractionError, 'nuspec file not found' unless entry - raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size - - Tempfile.open("nuget_extraction_package_file_#{package_file.id}") do |file| - entry.extract(file.path) { true } # allow #extract to overwrite the file - file.unlink - file.read - end - rescue Zip::EntrySizeError => e - raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}" - end - end + entry = package_zip_file.glob('*.nuspec').first + + raise ExtractionError, 'nuspec file not found' unless entry + raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size - def with_zip_file - package_file.file.use_open_file do |open_file| - zip_file = Zip::File.new(open_file, false, true) # rubocop:disable Performance/Rubyzip - yield(zip_file) + Tempfile.create('nuget_extraction_package_file') do |file| + entry.extract(file.path) { true } # allow #extract to overwrite the file + file.read end + rescue Zip::EntrySizeError => e + raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}" end end end diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb index 2c758a5ec20..53189063c85 100644 --- a/app/services/packages/nuget/metadata_extraction_service.rb +++ b/app/services/packages/nuget/metadata_extraction_service.rb @@ -23,10 +23,9 @@ module Packages end def nuspec_file_content - ExtractMetadataFileService + ProcessPackageFileService .new(package_file) - .execute - .payload + .execute[:nuspec_file_content] end end end diff --git a/app/services/packages/nuget/odata_package_entry_service.rb b/app/services/packages/nuget/odata_package_entry_service.rb index 0cdcc38de16..679b01d6c48 100644 --- a/app/services/packages/nuget/odata_package_entry_service.rb +++ b/app/services/packages/nuget/odata_package_entry_service.rb @@ -5,9 +5,6 @@ module Packages class OdataPackageEntryService include API::Helpers::RelatedResourcesHelpers - SEMVER_LATEST_VERSION_PLACEHOLDER = '0.0.0-latest-version' - LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT = 'latest' - def initialize(project, params) @project = project @params = params @@ -29,42 +26,40 @@ module Packages <title type='text'>#{params[:package_name]}</title> <content type='application/zip' src="#{download_url}"/> <m:properties> - <d:Version>#{package_version}</d:Version> + <d:Version>#{params[:package_version]}</d:Version> </m:properties> </entry> XML end - def package_version - params[:package_version] || SEMVER_LATEST_VERSION_PLACEHOLDER - end - def id_url expose_url "#{api_v4_projects_packages_nuget_v2_path(id: project.id)}" \ - "/Packages(Id='#{params[:package_name]}',Version='#{package_version}')" + "/Packages(Id='#{params[:package_name]}',Version='#{params[:package_version]}')" end - # TODO: use path helper when download endpoint is merged def download_url - expose_url "#{api_v4_projects_packages_nuget_v2_path(id: project.id)}" \ - "/download/#{params[:package_name]}/#{download_url_package_version}" - end - - def download_url_package_version - if latest_version? - LATEST_VERSION_FOR_V2_DOWNLOAD_ENDPOINT + if params[:package_version].present? + expose_url api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path( + { + id: project.id, + package_name: params[:package_name], + package_version: params[:package_version], + package_filename: file_name + }, + true + ) else - params[:package_version] + xml_base end end - def latest_version? - params[:package_version].nil? || params[:package_version] == SEMVER_LATEST_VERSION_PLACEHOLDER - end - def xml_base expose_url api_v4_projects_packages_nuget_v2_path(id: project.id) end + + def file_name + "#{params[:package_name]}.#{params[:package_version]}.nupkg" + end end end end diff --git a/app/services/packages/nuget/process_package_file_service.rb b/app/services/packages/nuget/process_package_file_service.rb new file mode 100644 index 00000000000..fa7a84ee3d6 --- /dev/null +++ b/app/services/packages/nuget/process_package_file_service.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Packages + module Nuget + class ProcessPackageFileService + ExtractionError = Class.new(StandardError) + NUGET_SYMBOL_FILE_EXTENSION = '.snupkg' + + def initialize(package_file) + @package_file = package_file + end + + def execute + raise ExtractionError, 'invalid package file' unless valid_package_file? + + nuspec_content = nil + + with_zip_file do |zip_file| + nuspec_content = nuspec_file_content(zip_file) + create_symbol_files(zip_file) if symbol_package_file? + end + + ServiceResponse.success(payload: { nuspec_file_content: nuspec_content }) + end + + private + + attr_reader :package_file + + def valid_package_file? + package_file && + package_file.package&.nuget? && + package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate + end + + def with_zip_file(&block) + package_file.file.use_open_file(unlink_early: false) do |open_file| + Zip::File.open(open_file.file_path, &block) # rubocop: disable Performance/Rubyzip + end + end + + def nuspec_file_content(zip_file) + ::Packages::Nuget::ExtractMetadataFileService + .new(zip_file) + .execute + .payload + end + + def create_symbol_files(zip_file) + ::Packages::Nuget::Symbols::CreateSymbolFilesService + .new(package_file.package, zip_file) + .execute + end + + def symbol_package_file? + package_file.file_name.end_with?(NUGET_SYMBOL_FILE_EXTENSION) + end + end + end +end diff --git a/app/services/packages/nuget/symbols/create_symbol_files_service.rb b/app/services/packages/nuget/symbols/create_symbol_files_service.rb new file mode 100644 index 00000000000..03e14ba00e1 --- /dev/null +++ b/app/services/packages/nuget/symbols/create_symbol_files_service.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Packages + module Nuget + module Symbols + class CreateSymbolFilesService + ExtractionError = Class.new(StandardError) + SYMBOL_ENTRIES_LIMIT = 100 + CONTENT_TYPE = 'application/octet-stream' + + def initialize(package, package_zip_file) + @package = package + @symbol_entries = package_zip_file.glob('**/*.pdb') + end + + def execute + return if symbol_entries.empty? + + process_symbol_entries + rescue ExtractionError => e + Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id) + end + + private + + attr_reader :package, :symbol_entries + + def process_symbol_entries + Tempfile.create('nuget_extraction_symbol_file') do |tmp_file| + symbol_entries.each_with_index do |entry, index| + raise ExtractionError, 'too many symbol entries' if index >= SYMBOL_ENTRIES_LIMIT + + entry.extract(tmp_file.path) { true } + File.open(tmp_file.path) do |file| + create_symbol(entry.name, file) + end + end + end + rescue Zip::EntrySizeError => e + raise ExtractionError, "symbol file has the wrong entry size: #{e.message}" + rescue Zip::EntryNameError => e + raise ExtractionError, "symbol file has the wrong entry name: #{e.message}" + end + + def create_symbol(path, file) + signature = extract_signature(file.read(1.kilobyte)) + return if signature.blank? + + ::Packages::Nuget::Symbol.create!( + package: package, + file: { tempfile: file, filename: path.downcase, content_type: CONTENT_TYPE }, + file_path: path, + signature: signature, + size: file.size + ) + rescue StandardError => e + Gitlab::ErrorTracking.log_exception(e, class: self.class.name, package_id: package.id) + end + + def extract_signature(content_fragment) + ExtractSymbolSignatureService + .new(content_fragment) + .execute + .payload + end + end + end + end +end diff --git a/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb b/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb new file mode 100644 index 00000000000..c2ccdb517b5 --- /dev/null +++ b/app/services/packages/nuget/symbols/extract_symbol_signature_service.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Packages + module Nuget + module Symbols + class ExtractSymbolSignatureService + include Gitlab::Utils::StrongMemoize + + # More information about the GUID format can be found here: + # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#key-formatting-basic-rules + GUID_START_INDEX = 7 + GUID_END_INDEX = 22 + GUID_PARTS_LENGTHS = [4, 2, 2, 8].freeze + GUID_AGE_PART = 'FFFFFFFF' + TWO_CHARACTER_HEX_REGEX = /\h{2}/ + + # The extraction of the signature in this service is based on the following documentation: + # https://github.com/dotnet/symstore/blob/main/docs/specs/SSQP_Key_Conventions.md#portable-pdb-signature + + def initialize(symbol_content) + @symbol_content = symbol_content + end + + def execute + return error_response unless signature + + ServiceResponse.success(payload: signature) + end + + private + + attr_reader :symbol_content + + def signature + # Find the index of the first occurrence of 'Blob' + guid_index = symbol_content.index('Blob') + return if guid_index.nil? + + # Extract the binary GUID from the symbol content + guid = symbol_content[(guid_index + GUID_START_INDEX)..(guid_index + GUID_END_INDEX)] + return if guid.nil? + + # Convert the GUID into an array of two-character hex strings + guid = guid.unpack('H*').flat_map { |el| el.scan(TWO_CHARACTER_HEX_REGEX) } + + # Reorder the GUID parts based on arbitrary lengths + guid = GUID_PARTS_LENGTHS.map { |length| guid.shift(length) } + + # Concatenate the parts of the GUID back together + result = guid.first(3).map(&:reverse) + result << guid.last + result = result.join + result << GUID_AGE_PART + end + strong_memoize_attr :signature + + def error_response + ServiceResponse.error(message: 'Could not find the signature in the symbol file') + end + end + end + end +end diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb index 258f8c8f6aa..4cec4ed2fae 100644 --- a/app/services/packages/nuget/update_package_from_metadata_service.rb +++ b/app/services/packages/nuget/update_package_from_metadata_service.rb @@ -54,13 +54,16 @@ module Packages update_linked_package end - update_package(target_package) + build_infos = package_to_destroy&.build_infos || [] + + update_package(target_package, build_infos) + update_symbol_files(target_package, package_to_destroy) if symbol_package? ::Packages::UpdatePackageFileService.new(@package_file, package_id: target_package.id, file_name: package_filename) .execute package_to_destroy&.destroy! end - def update_package(package) + def update_package(package, build_infos) return if symbol_package? ::Packages::Nuget::SyncMetadatumService @@ -71,28 +74,21 @@ module Packages .new(package, package_tags) .execute + package.build_infos << build_infos if build_infos.any? rescue StandardError => e raise InvalidMetadataError, e.message end + def update_symbol_files(package, package_to_destroy) + package_to_destroy.nuget_symbols.update_all(package_id: package.id) + end + def valid_metadata? fields = [package_name, package_version, package_description] fields << package_authors unless symbol_package? fields.all?(&:present?) end - def link_to_existing_package - package_to_destroy = @package_file.package - # Updating package_id updates the path where the file is stored. - # We must pass the file again so that CarrierWave can handle the update - @package_file.update!( - package_id: existing_package.id, - file: @package_file.file - ) - package_to_destroy.destroy! - existing_package - end - def update_linked_package @package_file.package.update!( name: package_name, @@ -106,12 +102,15 @@ module Packages end def existing_package - @package_file.project.packages - .nuget - .with_name(package_name) - .with_version(package_version) - .not_pending_destruction - .first + ::Packages::Nuget::PackageFinder + .new( + nil, + @package_file.project, + package_name: package_name, + package_version: package_version + ) + .execute + .first end strong_memoize_attr :existing_package diff --git a/app/services/packages/protection/create_rule_service.rb b/app/services/packages/protection/create_rule_service.rb new file mode 100644 index 00000000000..e69eb8faf60 --- /dev/null +++ b/app/services/packages/protection/create_rule_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Packages + module Protection + class CreateRuleService < BaseProjectService + ALLOWED_ATTRIBUTES = %i[ + package_name_pattern + package_type + push_protected_up_to_access_level + ].freeze + + def execute + unless can?(current_user, :admin_package, project) + error_message = _('Unauthorized to create a package protection rule') + return service_response_error(message: error_message) + end + + package_protection_rule = project.package_protection_rules.create(params.slice(*ALLOWED_ATTRIBUTES)) + + unless package_protection_rule.persisted? + return service_response_error(message: package_protection_rule.errors.full_messages) + end + + ServiceResponse.success(payload: { package_protection_rule: package_protection_rule }) + rescue StandardError => e + service_response_error(message: e.message) + end + + private + + def service_response_error(message:) + ServiceResponse.error( + message: message, + payload: { package_protection_rule: nil } + ) + end + end + end +end diff --git a/app/services/pages/migrate_from_legacy_storage_service.rb b/app/services/pages/migrate_from_legacy_storage_service.rb deleted file mode 100644 index d102f93e863..00000000000 --- a/app/services/pages/migrate_from_legacy_storage_service.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -module Pages - class MigrateFromLegacyStorageService - def initialize(logger, ignore_invalid_entries:, mark_projects_as_not_deployed:) - @logger = logger - @ignore_invalid_entries = ignore_invalid_entries - @mark_projects_as_not_deployed = mark_projects_as_not_deployed - - @migrated = 0 - @errored = 0 - @counters_lock = Mutex.new - end - - def execute_with_threads(threads:, batch_size:) - @queue = SizedQueue.new(1) - - migration_threads = start_migration_threads(threads) - - ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: batch_size) do |batch| - @queue.push(batch) - end - - @queue.close - - @logger.info(message: "Pages legacy storage migration: Waiting for threads to finish...") - migration_threads.each(&:join) - - { migrated: @migrated, errored: @errored } - end - - def execute_for_batch(project_ids) - batch = ProjectPagesMetadatum.only_on_legacy_storage.where(project_id: project_ids) # rubocop: disable CodeReuse/ActiveRecord - - process_batch(batch) - - { migrated: @migrated, errored: @errored } - end - - private - - def start_migration_threads(count) - Array.new(count) do - Thread.new do - while batch = @queue.pop - Rails.application.executor.wrap do - process_batch(batch) - end - end - end - end - end - - def process_batch(batch) - batch.with_project_route_and_deployment.each do |metadatum| - project = metadatum.project - - migrate_project(project) - end - - @logger.info(message: "Pages legacy storage migration: batch processed", migrated: @migrated, errored: @errored) - rescue StandardError => e - # This method should never raise exception otherwise all threads might be killed - # and this will result in queue starving (and deadlock) - Gitlab::ErrorTracking.track_exception(e) - @logger.error(message: "Pages legacy storage migration: failed processing a batch: #{e.message}") - end - - def migrate_project(project) - result = nil - time = Benchmark.realtime do - result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project, - ignore_invalid_entries: @ignore_invalid_entries, - mark_projects_as_not_deployed: @mark_projects_as_not_deployed).execute - end - - if result[:status] == :success - @logger.info(message: "Pages legacy storage migration: project migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2)) - @counters_lock.synchronize { @migrated += 1 } - else - @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2)) - @counters_lock.synchronize { @errored += 1 } - end - rescue StandardError => e - @counters_lock.synchronize { @errored += 1 } - @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project&.id, pages_path: project&.pages_path) - Gitlab::ErrorTracking.track_exception(e, project_id: project&.id) - end - end -end diff --git a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb deleted file mode 100644 index 9c1671fbc15..00000000000 --- a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Pages - class MigrateLegacyStorageToDeploymentService - include BaseServiceUtility - - attr_reader :project - - def initialize(project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) - @project = project - @ignore_invalid_entries = ignore_invalid_entries - @mark_projects_as_not_deployed = mark_projects_as_not_deployed - end - - def execute - zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute - - if zip_result[:status] == :error - return error("Can't create zip archive: #{zip_result[:message]}") - end - - archive_path = zip_result[:archive_path] - - unless archive_path - return error("Archive not created. Missing public directory in #{@project.pages_path}") unless @mark_projects_as_not_deployed - - project.set_first_pages_deployment!(nil) - - return success( - message: "Archive not created. Missing public directory in #{project.pages_path}? Marked project as not deployed") - end - - deployment = nil - File.open(archive_path) do |file| - deployment = project.pages_deployments.create!( - file: file, - file_count: zip_result[:entries_count], - file_sha256: Digest::SHA256.file(archive_path).hexdigest - ) - end - - project.set_first_pages_deployment!(deployment) - - success - ensure - FileUtils.rm_f(archive_path) if archive_path - end - end -end diff --git a/app/services/pages/zip_directory_service.rb b/app/services/pages/zip_directory_service.rb deleted file mode 100644 index c9029b9666a..00000000000 --- a/app/services/pages/zip_directory_service.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -module Pages - class ZipDirectoryService - include BaseServiceUtility - include Gitlab::Utils::StrongMemoize - - # used only to track exceptions in Sentry - InvalidEntryError = Class.new(StandardError) - - PUBLIC_DIR = 'public' - - attr_reader :public_dir, :real_dir - - def initialize(input_dir, ignore_invalid_entries: false) - @input_dir = input_dir - @ignore_invalid_entries = ignore_invalid_entries - end - - def execute - return success unless resolve_public_dir - - output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects - - FileUtils.rm_f(output_file) - - entries_count = 0 - # Since we're writing not reading here, we can safely silence the cop. - # It currently cannot discern between opening for reading or writing. - ::Zip::File.open(output_file, ::Zip::File::CREATE) do |zipfile| # rubocop:disable Performance/Rubyzip - write_entry(zipfile, PUBLIC_DIR) - entries_count = zipfile.entries.count - end - - success(archive_path: output_file, entries_count: entries_count) - rescue StandardError => e - FileUtils.rm_f(output_file) if output_file - raise e - end - - private - - def resolve_public_dir - @real_dir = File.realpath(@input_dir) - @public_dir = File.join(real_dir, PUBLIC_DIR) - - valid_path?(public_dir) - rescue Errno::ENOENT - false - end - - def write_entry(zipfile, zipfile_path) - disk_file_path = File.join(real_dir, zipfile_path) - - unless valid_path?(disk_file_path) - # archive with invalid entry will just have this entry missing - raise InvalidEntryError, "#{disk_file_path} is invalid, input_dir: #{@input_dir}" - end - - ftype = File.lstat(disk_file_path).ftype - case ftype - when 'directory' - recursively_zip_directory(zipfile, disk_file_path, zipfile_path) - when 'file', 'link' - zipfile.add(zipfile_path, disk_file_path) - else - raise InvalidEntryError, "#{disk_file_path} has invalid ftype: #{ftype}, input_dir: #{@input_dir}" - end - rescue Errno::ENOENT, Errno::ELOOP, InvalidEntryError => e - Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path) - - raise e unless @ignore_invalid_entries - end - - def recursively_zip_directory(zipfile, disk_file_path, zipfile_path) - zipfile.mkdir(zipfile_path) - - entries = Dir.entries(disk_file_path) - %w[. ..] - entries = entries.map { |entry| File.join(zipfile_path, entry) } - - write_entries(zipfile, entries) - end - - def write_entries(zipfile, entries) - entries.each do |zipfile_path| - write_entry(zipfile, zipfile_path) - end - end - - # SafeZip was introduced only recently, - # so we have invalid entries on disk - def valid_path?(disk_file_path) - realpath = File.realpath(disk_file_path) - realpath == public_dir || realpath.start_with?(public_dir + "/") - end - end -end diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb index 9dc957b5be2..93d68eec3bc 100644 --- a/app/services/projects/after_rename_service.rb +++ b/app/services/projects/after_rename_service.rb @@ -62,13 +62,9 @@ module Projects def rename_or_migrate_repository! success = - if migrate_to_hashed_storage? - ::Projects::HashedStorage::MigrationService - .new(project, full_path_before) - .execute - else - project.storage.rename_repo(old_full_path: full_path_before, new_full_path: full_path_after) - end + ::Projects::HashedStorage::MigrationService + .new(project, full_path_before) + .execute rename_failed! unless success end @@ -105,11 +101,6 @@ module Projects ) end - def migrate_to_hashed_storage? - Gitlab::CurrentSettings.hashed_storage_enabled? && - project.storage_upgradable? - end - def send_move_instructions? !project.import_started? end @@ -147,5 +138,3 @@ module Projects end end end - -Projects::AfterRenameService.prepend_mod_with('Projects::AfterRenameService') diff --git a/app/services/projects/container_repository/cleanup_tags_base_service.rb b/app/services/projects/container_repository/cleanup_tags_base_service.rb index 61b09de1643..45557d03502 100644 --- a/app/services/projects/container_repository/cleanup_tags_base_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb @@ -100,7 +100,7 @@ module Projects def older_than_in_seconds strong_memoize(:older_than_in_seconds) do - ChronicDuration.parse(older_than, use_complete_matcher: true).seconds + ChronicDuration.parse(older_than).seconds end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index e4987438c57..e0ee3683ac8 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -229,7 +229,7 @@ module Projects %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424281' ) do ApplicationRecord.transaction do - @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data + @project.build_or_assign_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data # Avoid project callbacks being triggered multiple times by saving the parent first. # See https://github.com/rails/rails/issues/41701. diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb index f77bae71d63..c9642fb495a 100644 --- a/app/services/projects/group_links/create_service.rb +++ b/app/services/projects/group_links/create_service.rb @@ -16,7 +16,7 @@ module Projects delegate :root_ancestor, to: :project def valid_to_create? - can?(current_user, :read_namespace, shared_with_group) && sharing_allowed? + can?(current_user, :read_namespace_via_membership, shared_with_group) && sharing_allowed? end def build_link diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb deleted file mode 100644 index 6241a3e144f..00000000000 --- a/app/services/projects/hashed_storage/base_repository_service.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -module Projects - module HashedStorage - # Returned when repository can't be made read-only because there is already a git transfer in progress - RepositoryInUseError = Class.new(StandardError) - - class BaseRepositoryService < BaseService - include Gitlab::ShellAdapter - - attr_reader :old_disk_path, :new_disk_path, :old_storage_version, - :logger, :move_wiki, :move_design - - def initialize(project:, old_disk_path:, logger: nil) - @project = project - @logger = logger || Gitlab::AppLogger - @old_disk_path = old_disk_path - @move_wiki = has_wiki? - @move_design = has_design? - end - - protected - - def has_wiki? - gitlab_shell.repository_exists?(project.repository_storage, "#{old_wiki_disk_path}.git") - end - - def has_design? - gitlab_shell.repository_exists?(project.repository_storage, "#{old_design_disk_path}.git") - end - - def move_repository(from_name, to_name) - from_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{from_name}.git") - to_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{to_name}.git") - - # If we don't find the repository on either original or target we should log that as it could be an issue if the - # project was not originally empty. - if !from_exists && !to_exists - logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..." - - # We return true so we still reflect the change in the database. - # Next time the repository is (re)created it will be under the new storage layout - return true - elsif !from_exists - # Repository have been moved already. - return true - end - - gitlab_shell.mv_repository(project.repository_storage, from_name, to_name).tap do |moved| - if moved - logger.info("Repository moved from '#{from_name}' to '#{to_name}' (PROJECT_ID=#{project.id})") - else - logger.error("Repository cannot be moved from '#{from_name}' to '#{to_name}' (PROJECT_ID=#{project.id})") - end - end - end - - def move_repositories - result = move_repository(old_disk_path, new_disk_path) - project.reload_repository! - - if move_wiki - result &&= move_repository(old_wiki_disk_path, new_wiki_disk_path) - project.clear_memoization(:wiki) - end - - if move_design - result &&= move_repository(old_design_disk_path, new_design_disk_path) - project.clear_memoization(:design_repository) - end - - result - end - - def rollback_folder_move - move_repository(new_disk_path, old_disk_path) - move_repository(new_wiki_disk_path, old_wiki_disk_path) - move_repository(new_design_disk_path, old_design_disk_path) if move_design - end - - def try_to_set_repository_read_only! - project.set_repository_read_only! - rescue Project::RepositoryReadOnlyError => err - migration_error = "Target repository '#{old_disk_path}' cannot be made read-only: #{err.message}" - logger.error migration_error - - raise RepositoryInUseError, migration_error - end - - def wiki_path_suffix - @wiki_path_suffix ||= Gitlab::GlRepository::WIKI.path_suffix - end - - def old_wiki_disk_path - @old_wiki_disk_path ||= "#{old_disk_path}#{wiki_path_suffix}" - end - - def new_wiki_disk_path - @new_wiki_disk_path ||= "#{new_disk_path}#{wiki_path_suffix}" - end - - def design_path_suffix - @design_path_suffix ||= ::Gitlab::GlRepository::DESIGN.path_suffix - end - - def old_design_disk_path - @old_design_disk_path ||= "#{old_disk_path}#{design_path_suffix}" - end - - def new_design_disk_path - @new_design_disk_path ||= "#{new_disk_path}#{design_path_suffix}" - end - end - end -end diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb deleted file mode 100644 index b65d0e63fd3..00000000000 --- a/app/services/projects/hashed_storage/migrate_repository_service.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module Projects - module HashedStorage - class MigrateRepositoryService < BaseRepositoryService - def execute - try_to_set_repository_read_only! - - @old_storage_version = project.storage_version - project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository] - - @new_disk_path = project.disk_path - - result = move_repositories - - if result - project.set_full_path - project.track_project_repository - else - rollback_folder_move - project.storage_version = nil - end - - project.transaction do - project.save!(validate: false) - project.set_repository_writable! - end - - result - rescue Gitlab::Git::CommandError => e - logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}") - - rollback_migration! - - false - rescue OpenSSL::Cipher::CipherError => e - logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}") - - rollback_migration! - - false - end - - private - - def rollback_migration! - rollback_folder_move - project.storage_version = nil - project.set_repository_writable! - end - end - end -end - -Projects::HashedStorage::MigrateRepositoryService.prepend_mod_with('Projects::HashedStorage::MigrateRepositoryService') diff --git a/app/services/projects/hashed_storage/migration_service.rb b/app/services/projects/hashed_storage/migration_service.rb index 57a775a8f9e..e2015a4cca6 100644 --- a/app/services/projects/hashed_storage/migration_service.rb +++ b/app/services/projects/hashed_storage/migration_service.rb @@ -12,11 +12,6 @@ module Projects end def execute - # Migrate repository from Legacy to Hashed Storage - unless project.hashed_storage?(:repository) - return false unless migrate_repository_service.execute - end - # Migrate attachments from Legacy to Hashed Storage unless project.hashed_storage?(:attachments) return false unless migrate_attachments_service.execute @@ -27,10 +22,6 @@ module Projects private - def migrate_repository_service - HashedStorage::MigrateRepositoryService.new(project: project, old_disk_path: old_disk_path, logger: logger) - end - def migrate_attachments_service HashedStorage::MigrateAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger) end diff --git a/app/services/projects/hashed_storage/rollback_attachments_service.rb b/app/services/projects/hashed_storage/rollback_attachments_service.rb deleted file mode 100644 index 4bb8cb605a3..00000000000 --- a/app/services/projects/hashed_storage/rollback_attachments_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Projects - module HashedStorage - class RollbackAttachmentsService < BaseAttachmentService - def execute - origin = FileUploader.absolute_base_dir(project) - - project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository] - target = FileUploader.absolute_base_dir(project) - - @new_disk_path = FileUploader.base_dir(project) - - result = move_folder!(origin, target) - - if result - project.save!(validate: false) - - yield if block_given? - else - # Rollback changes - project.rollback! - end - - result - end - end - end -end diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb deleted file mode 100644 index f4146ff9158..00000000000 --- a/app/services/projects/hashed_storage/rollback_repository_service.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Projects - module HashedStorage - class RollbackRepositoryService < BaseRepositoryService - def execute - try_to_set_repository_read_only! - - @old_storage_version = project.storage_version - project.storage_version = nil - - @new_disk_path = project.disk_path - - result = move_repositories - - if result - project.set_full_path - project.track_project_repository - else - rollback_folder_move - project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository] - end - - project.transaction do - project.save!(validate: false) - project.set_repository_writable! - end - - result - rescue Gitlab::Git::CommandError => e - logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}") - - rollback_migration! - - false - rescue OpenSSL::Cipher::CipherError => e - logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}") - - rollback_migration! - - false - end - - private - - def rollback_migration! - rollback_folder_move - project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository] - project.set_repository_writable! - end - end - end -end diff --git a/app/services/projects/hashed_storage/rollback_service.rb b/app/services/projects/hashed_storage/rollback_service.rb deleted file mode 100644 index 01b343a12d1..00000000000 --- a/app/services/projects/hashed_storage/rollback_service.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module Projects - module HashedStorage - class RollbackService < BaseService - attr_reader :logger, :old_disk_path - - def initialize(project, old_disk_path, logger: nil) - @project = project - @old_disk_path = old_disk_path - @logger = logger || Gitlab::AppLogger - end - - def execute - # Rollback attachments from Hashed Storage to Legacy - if project.hashed_storage?(:attachments) - return false unless rollback_attachments_service.execute - end - - # Rollback repository from Hashed Storage to Legacy - if project.hashed_storage?(:repository) - rollback_repository_service.execute - end - end - - private - - def rollback_attachments_service - HashedStorage::RollbackAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger) - end - - def rollback_repository_service - HashedStorage::RollbackRepositoryService.new(project: project, old_disk_path: old_disk_path, logger: logger) - end - end - end -end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index e22b728cea3..fde56d8429e 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -29,7 +29,7 @@ module Projects after_execute_hook success - rescue Gitlab::UrlBlocker::BlockedUrlError, StandardError => e + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError, StandardError => e Gitlab::Import::ImportFailureService.track( project_id: project.id, error_source: self.class.name, @@ -76,7 +76,7 @@ module Projects if project.external_import? && !unknown_url? begin @resolved_address = get_resolved_address - rescue Gitlab::UrlBlocker::BlockedUrlError => e + rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e raise e, s_("ImportProjects|Blocked import URL: %{message}") % { message: e.message } end end diff --git a/app/services/projects/in_product_marketing_campaign_emails_service.rb b/app/services/projects/in_product_marketing_campaign_emails_service.rb deleted file mode 100644 index a549d8f594e..00000000000 --- a/app/services/projects/in_product_marketing_campaign_emails_service.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Projects - class InProductMarketingCampaignEmailsService - include Gitlab::Experiment::Dsl - - def initialize(project, campaign) - @project = project - @campaign = campaign - @sent_email_records = ::Users::InProductMarketingEmailRecords.new - end - - def execute - send_emails - end - - private - - attr_reader :project, :campaign, :sent_email_records - - def send_emails - project_users.each do |user| - send_email(user) - end - - sent_email_records.save! - end - - def project_users - @project_users ||= project.users.merge(Users::InProductMarketingEmail.without_campaign(campaign)) - end - - def project_users_max_access_levels - ids = project_users.map(&:id) - @project_users_max_access_levels ||= project.team.max_member_access_for_user_ids(ids) - end - - def send_email(user) - return unless user.can?(:receive_notifications) - return unless target_user?(user) - - Notify.build_ios_app_guide_email(user.notification_email_or_default).deliver_later - - sent_email_records.add(user, campaign: campaign) - experiment(:build_ios_app_guide_email, project: project).track(:email_sent) - end - - def target_user?(user) - max_access_level = project_users_max_access_levels[user.id] - max_access_level >= Gitlab::Access::DEVELOPER - end - end -end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 458eaec4e2e..fe19d1f051d 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -11,8 +11,8 @@ module Projects noteable_owner + participants_in_noteable + all_members + - groups + - project_members + project_members + + groups render_participants_as_hash(participants.uniq) end diff --git a/app/services/projects/record_target_platforms_service.rb b/app/services/projects/record_target_platforms_service.rb index 664e72e9785..d43b587154b 100644 --- a/app/services/projects/record_target_platforms_service.rb +++ b/app/services/projects/record_target_platforms_service.rb @@ -28,26 +28,11 @@ module Projects project_setting.target_platforms = target_platforms project_setting.save - - send_build_ios_app_guide_email - project_setting.target_platforms end def project_setting @project_setting ||= ::ProjectSetting.find_or_initialize_by(project: project) # rubocop:disable CodeReuse/ActiveRecord end - - def experiment_candidate? - experiment(:build_ios_app_guide_email, project: project).run - end - - def send_build_ios_app_guide_email - return unless target_platforms.include? :ios - return unless experiment_candidate? - - campaign = Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE - Projects::InProductMarketingCampaignEmailsService.new(project, campaign).execute - end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 3d08039942b..30d9e1922cc 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -124,9 +124,6 @@ module Projects # Notifications project.send_move_instructions(@old_path) - # Directories on disk - move_project_folders(project) - transfer_missing_group_resources(@old_group) # Move uploads @@ -235,44 +232,15 @@ module Projects end def rollback_side_effects - rollback_folder_move project.reset update_namespace_and_visibility(@old_namespace) update_repository_configuration(@old_path) end - def rollback_folder_move - return if project.hashed_storage?(:repository) - - move_repo_folder(@new_path, @old_path) - move_repo_folder(new_wiki_repo_path, old_wiki_repo_path) - move_repo_folder(new_design_repo_path, old_design_repo_path) - end - - def move_repo_folder(from_name, to_name) - gitlab_shell.mv_repository(project.repository_storage, from_name, to_name) - end - def execute_system_hooks system_hook_service.execute_hooks_for(project, :transfer) end - def move_project_folders(project) - return if project.hashed_storage?(:repository) - - # Move main repository - unless move_repo_folder(@old_path, @new_path) - raise TransferError, s_("TransferProject|Cannot move project") - end - - # Disk path is changed; we need to ensure we reload it - project.reload_repository! - - # Move wiki and design repos also if present - move_repo_folder(old_wiki_repo_path, new_wiki_repo_path) - move_repo_folder(old_design_repo_path, new_design_repo_path) - end - def move_project_uploads(project) return if project.hashed_storage?(:attachments) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index dc92c501b8c..ab38efff7c9 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -109,11 +109,6 @@ module Projects PagesDeployment.deactivate_deployments_older_than( deployment, time: OLD_DEPLOYMENTS_DESTRUCTION_DELAY.from_now) - - DestroyPagesDeploymentsWorker.perform_in( - OLD_DEPLOYMENTS_DESTRUCTION_DELAY, - project.id, - deployment.id) end def register_attempt diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb index 799ae5677c3..85fb1890fcd 100644 --- a/app/services/projects/update_repository_storage_service.rb +++ b/app/services/projects/update_repository_storage_service.rb @@ -48,13 +48,8 @@ module Projects pool_repository: pool_repository ) - checksum, new_checksum = replicate_object_pool_repository(from: pool_repository, to: target_pool_repository) - - if checksum != new_checksum - raise Error, - format(s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}'), - type: 'object_pool', old: checksum, new: new_checksum) - end + Repositories::ReplicateService.new(pool_repository.object_pool.repository) + .execute(target_pool_repository.object_pool.repository, :object_pool) end def remove_old_paths @@ -96,19 +91,6 @@ module Projects ) end - def replicate_object_pool_repository(from:, to:) - old_object_pool = from.object_pool - new_object_pool = to.object_pool - - checksum = old_object_pool.repository.checksum - - new_object_pool.repository.replicate(old_object_pool.repository) - - new_checksum = new_object_pool.repository.checksum - - [checksum, new_checksum] - end - def replicate_object_pool_on_move_ff_enabled? Feature.enabled?(:replicate_object_pool_on_move, project) end diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index f0243d844d9..95e0861a37a 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -19,7 +19,7 @@ module Releases return tag unless tag.is_a?(Gitlab::Git::Tag) if project.catalog_resource - response = Ci::Catalog::ValidateResourceService.new(project, ref).execute + response = Ci::Catalog::Resources::ValidateService.new(project, ref).execute return error(response.message) if response.error? end diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb index 41b421662ef..78613c05ff1 100644 --- a/app/services/releases/destroy_service.rb +++ b/app/services/releases/destroy_service.rb @@ -9,6 +9,8 @@ module Releases if release.destroy update_catalog_resource! + execute_hooks(release, 'delete') + success(tag: existing_tag, release: release) else error(release.errors.messages || '400 Bad request', 400) diff --git a/app/services/repositories/base_service.rb b/app/services/repositories/base_service.rb index bf7ac2e5fd8..371ff2fc499 100644 --- a/app/services/repositories/base_service.rb +++ b/app/services/repositories/base_service.rb @@ -15,10 +15,6 @@ class Repositories::BaseService < BaseService gitlab_shell.repository_exists?(repository.shard, path + '.git') end - def mv_repository(from_path, to_path) - gitlab_shell.mv_repository(repository.shard, from_path, to_path) - end - # If we get a Gitaly error, the repository may be corrupted. We can # ignore these errors since we're going to trash the repositories # anyway. diff --git a/app/services/repositories/replicate_service.rb b/app/services/repositories/replicate_service.rb new file mode 100644 index 00000000000..0148223910f --- /dev/null +++ b/app/services/repositories/replicate_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Repositories + class ReplicateService < Repositories::BaseService + Error = Class.new(StandardError) + + def execute(new_repository, type) + new_repository.replicate(repository) + + new_checksum = new_repository.checksum + checksum = repository.checksum + + return if new_checksum == checksum + + raise Error, format(s_( + 'ReplicateService|Failed to verify %{type} repository checksum from %{old} to %{new}' + ), type: type, old: checksum, new: new_checksum) + rescue StandardError => e + new_repository.remove + + raise e + end + end +end diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 9efe51b43b8..2d4bebc8b2b 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -90,7 +90,7 @@ module Spam end def allow_possible_spam? - target.allow_possible_spam?(user) || user.allow_possible_spam? + target.allow_possible_spam?(user) || user.trusted? end def spamcheck_client diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 04ae734a8fe..8442ff81d41 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -24,15 +24,16 @@ module SystemNotes end # - # noteable_ref - Referenced noteable object + # noteable_ref - Referenced noteable object, or array of objects # # Example Note text: # # "marked this issue as related to gitlab-foss#9001" + # "marked this issue as related to gitlab-foss#9001, gitlab-foss#9002, and gitlab-foss#9003" # # Returns the created Note object def relate_issuable(noteable_ref) - body = "marked this #{noteable_name} as related to #{noteable_ref.to_reference(noteable.resource_parent)}" + body = "marked this #{noteable_name} as related to #{extract_issuable_reference(noteable_ref)}" track_issue_event(:track_issue_related_action) @@ -539,6 +540,14 @@ module SystemNotes name.humanize(capitalize: false) end + + def extract_issuable_reference(reference) + if reference.is_a?(Array) + reference.map { |item| item.to_reference(noteable.resource_parent) }.to_sentence + else + reference.to_reference(noteable.resource_parent) + end + end end end diff --git a/app/services/tasks_to_be_done/base_service.rb b/app/services/tasks_to_be_done/base_service.rb deleted file mode 100644 index 1d50e5081ff..00000000000 --- a/app/services/tasks_to_be_done/base_service.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module TasksToBeDone - class BaseService < ::BaseContainerService - LABEL_PREFIX = 'tasks to be done' - - def initialize(container:, current_user:, assignee_ids: []) - params = { - assignee_ids: assignee_ids, - title: title, - description: description, - add_labels: label_name - } - super(container: container, current_user: current_user, params: params) - end - - def execute - if (issue = existing_task_issue) - update_service = Issues::UpdateService.new(container: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] }) - update_service.execute(issue) - else - create_service = Issues::CreateService.new(container: project, current_user: current_user, params: params, perform_spam_check: false) - create_service.execute - end - end - - private - - def existing_task_issue - IssuesFinder.new( - current_user, - project_id: project.id, - state: 'opened', - non_archived: true, - label_name: label_name - ).execute.last - end - - def title - raise NotImplementedError - end - - def description - raise NotImplementedError - end - - def label_suffix - raise NotImplementedError - end - - def label_name - "#{LABEL_PREFIX}:#{label_suffix}" - end - end -end diff --git a/app/services/tasks_to_be_done/create_ci_task_service.rb b/app/services/tasks_to_be_done/create_ci_task_service.rb deleted file mode 100644 index 025ca2feb8e..00000000000 --- a/app/services/tasks_to_be_done/create_ci_task_service.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module TasksToBeDone - class CreateCiTaskService < BaseService - protected - - def title - 'Set up CI/CD' - end - - def description - <<~DESCRIPTION - GitLab CI/CD is a tool built into GitLab for software development through the [continuous methodologies](https://docs.gitlab.com/ee/ci/introduction/index.html#introduction-to-cicd-methodologies): - - * Continuous Integration (CI) - * Continuous Delivery (CD) - * Continuous Deployment (CD) - - Continuous Integration works by pushing small changes to your application’s codebase hosted in a Git repository, and, to every push, run a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch. - - Continuous Delivery and Deployment consist of a step further CI, deploying your application to production at every push to the default branch of the repository. - - These methodologies allow you to catch bugs and errors early in the development cycle, ensuring that all the code deployed to production complies with the code standards you established for your app. - - * :book: [Read the documentation](https://docs.gitlab.com/ee/ci/introduction/index.html) - * :clapper: [Watch a Demo](https://www.youtube.com/watch?v=1iXFbchozdY) - - ## Next steps - - * [ ] To start we recommend reviewing the following documentation: - * [ ] [How GitLab CI/CD works.](https://docs.gitlab.com/ee/ci/introduction/index.html#how-gitlab-cicd-works) - * [ ] [Fundamental pipeline architectures.](https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html) - * [ ] [GitLab CI/CD basic workflow.](https://docs.gitlab.com/ee/ci/introduction/index.html#basic-cicd-workflow) - * [ ] [Step-by-step guide for writing .gitlab-ci.yml for the first time.](https://docs.gitlab.com/ee/user/project/pages/getting_started_part_four.html) - * [ ] When you're ready select **Projects** (in the top navigation bar) > **Your projects** > select the Project you've already created. - * [ ] Select **CI / CD** in the left navigation to start setting up CI / CD in your project. - DESCRIPTION - end - - def label_suffix - 'ci' - end - end -end diff --git a/app/services/tasks_to_be_done/create_code_task_service.rb b/app/services/tasks_to_be_done/create_code_task_service.rb deleted file mode 100644 index dc3b9366a66..00000000000 --- a/app/services/tasks_to_be_done/create_code_task_service.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module TasksToBeDone - class CreateCodeTaskService < BaseService - protected - - def title - 'Create or import your code into your Project (Repository)' - end - - def description - <<~DESCRIPTION - You've already created your Group and Project within GitLab; we'll quickly review this hierarchy below. Once you're within your project you can easily create or import repositories. - - **With GitLab Groups, you can:** - - * Create one or multiple Projects for hosting your codebase (repositories). - * Assemble related projects together. - * Grant members access to several projects at once. - - Groups can also be nested in subgroups. - - Read more about groups in our [documentation](https://docs.gitlab.com/ee/user/group/). - - **Within GitLab Projects, you can** - - * Use it as an issue tracker. - * Collaborate on code. - * Continuously build, test, and deploy your app with built-in GitLab CI/CD. - - You can also import an existing repository by providing the Git URL. - - * :book: [Read the documentation](https://docs.gitlab.com/ee/user/project/index.html). - - ## Next steps - - Create or import your first repository into the project you created: - - * [ ] Click **Projects** in the top navigation bar, then click **Your projects**. - * [ ] Select the Project that you created, then select **Repository**. - * [ ] Once on the Repository page you can select the **+** icon to add or import files. - * [ ] You can review our full documentation on creating [repositories](https://docs.gitlab.com/ee/user/project/repository/) in GitLab. - - :tada: All done, you can close this issue! - DESCRIPTION - end - - def label_suffix - 'code' - end - end -end diff --git a/app/services/tasks_to_be_done/create_issues_task_service.rb b/app/services/tasks_to_be_done/create_issues_task_service.rb deleted file mode 100644 index a2de6852868..00000000000 --- a/app/services/tasks_to_be_done/create_issues_task_service.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module TasksToBeDone - class CreateIssuesTaskService < BaseService - protected - - def title - 'Create/import issues (tickets) to collaborate on ideas and plan work' - end - - def description - <<~DESCRIPTION - Issues allow you and your team to discuss proposals before, and during, their implementation. They can be used for a variety of other purposes, customized to your needs and workflow. - - Issues are always associated with a specific project. If you have multiple projects in a group, you can view all the issues at the group level. [You can review our full Issue documentation here.](https://docs.gitlab.com/ee/user/project/issues/) - - If you have existing issues or equivalent tickets you can import them as long as they are formatted as a CSV file, [the import process is covered here](https://docs.gitlab.com/ee/user/project/issues/csv_import.html). - - **Common use cases include:** - - * Discussing the implementation of a new idea - * Tracking tasks and work status - * Accepting feature proposals, questions, support requests, or bug reports - * Elaborating on new code implementations - - ## Next steps - - * [ ] Select **Projects** in the top navigation > **Your Projects** > select the Project you've already created. - * [ ] Once you've selected that project, you can select **Issues** in the left navigation, then click **New issue**. - * [ ] Fill in the title and description in the **New issue** page. - * [ ] Click on **Create issue**. - - Pro tip: When you're in a group or project you can always utilize the **+** icon in the top navigation (located to the left of the search bar) to quickly create new issues. - - That's it! You can close this issue. - DESCRIPTION - end - - def label_suffix - 'issues' - end - end -end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 1f6cf2c83c9..be7405cc896 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -168,7 +168,7 @@ class TodoService def mark_todo(target, current_user) project = target.project attributes = attributes_for_todo(project, target, current_user, Todo::MARKED) - create_todos(current_user, attributes, project&.namespace, project) + create_todos(current_user, attributes, target_namespace(target), project) end def todo_exist?(issuable, current_user) @@ -338,7 +338,7 @@ class TodoService project = target.project assignees = target.assignees - old_assignees attributes = attributes_for_todo(project, target, author, Todo::ASSIGNED) - create_todos(assignees, attributes, project.namespace, project) + create_todos(assignees, attributes, target_namespace(target), project) end end @@ -386,6 +386,7 @@ class TodoService attributes.merge!(target_id: nil, commit_id: target.id) when Issue attributes[:issue_type] = target.issue_type + attributes[:group] = target.namespace if target.project.blank? when Discussion attributes.merge!(target_type: nil, target_id: nil, discussion: target) end @@ -469,6 +470,11 @@ class TodoService attributes end + + def target_namespace(target) + project = target.project + project&.namespace || target.try(:namespace) + end end TodoService.prepend_mod_with('TodoService') diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb index ed5c4df85b1..c9c86330e1c 100644 --- a/app/services/todos/destroy/base_service.rb +++ b/app/services/todos/destroy/base_service.rb @@ -4,37 +4,6 @@ module Todos module Destroy class BaseService def execute - return unless todos_to_remove? - - ::Gitlab::Database.allow_cross_joins_across_databases(url: - 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') do - without_authorized(todos).delete_all - end - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def without_authorized(items) - items.where.not('todos.user_id' => authorized_users) - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def authorized_users - ProjectAuthorization.select(:user_id).where(project_id: project_ids) - end - # rubocop: enable CodeReuse/ActiveRecord - - def todos - raise NotImplementedError - end - - def project_ids - raise NotImplementedError - end - - def todos_to_remove? raise NotImplementedError end end diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb index fadc76b1181..331c4a12681 100644 --- a/app/services/todos/destroy/confidential_issue_service.rb +++ b/app/services/todos/destroy/confidential_issue_service.rb @@ -9,58 +9,59 @@ module Todos # When issue_id is passed it deletes matching todos for one confidential issue. # When project_id is passed it deletes matching todos for all confidential issues of the project. class ConfidentialIssueService < ::Todos::Destroy::BaseService - extend ::Gitlab::Utils::Override - attr_reader :issues - # rubocop: disable CodeReuse/ActiveRecord def initialize(issue_id: nil, project_id: nil) @issues = if issue_id - Issue.where(id: issue_id) + Issue.id_in(issue_id) elsif project_id project_confidential_issues(project_id) end end - # rubocop: enable CodeReuse/ActiveRecord + + def execute + return unless todos_to_remove? + + ::Gitlab::Database.allow_cross_joins_across_databases( + url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') do + delete_todos + end + end private + def delete_todos + authorized_users = ProjectAuthorization.select(:user_id) + .for_project(project_ids) + .non_guests + + todos.not_in_users(authorized_users).delete_all + end + def project_confidential_issues(project_id) project = Project.find(project_id) project.issues.confidential_only end - override :todos # rubocop: disable CodeReuse/ActiveRecord def todos Todo.joins_issue_and_assignees - .where(target: issues) - .where(issues: { confidential: true }) + .for_target(issues) + .merge(Issue.confidential_only) .where('todos.user_id != issues.author_id') .where('todos.user_id != issue_assignees.user_id') end # rubocop: enable CodeReuse/ActiveRecord - override :todos_to_remove? def todos_to_remove? issues&.any?(&:confidential?) end - override :project_ids def project_ids issues&.distinct&.select(:project_id) end - - override :authorized_users - # rubocop: disable CodeReuse/ActiveRecord - def authorized_users - ProjectAuthorization.select(:user_id) - .where(project_id: project_ids) - .where('access_level >= ?', Gitlab::Access::REPORTER) - end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb index 60599ca9ca4..6962c204e0e 100644 --- a/app/services/todos/destroy/group_private_service.rb +++ b/app/services/todos/destroy/group_private_service.rb @@ -7,30 +7,34 @@ module Todos attr_reader :group - # rubocop: disable CodeReuse/ActiveRecord def initialize(group_id) - @group = Group.find_by(id: group_id) + @group = Group.find_by_id(group_id) + end + + def execute + return unless todos_to_remove? + + delete_todos end - # rubocop: enable CodeReuse/ActiveRecord private - override :todos - # rubocop: disable CodeReuse/ActiveRecord - def todos - Todo.where(group_id: group.id) + def delete_todos + authorized_users = Member.from_union( + [ + group.descendant_project_members_with_inactive.select(:user_id), + group.members_with_parents.select(:user_id) + ], + remove_duplicates: false + ).select(:user_id) + + todos.not_in_users(authorized_users).delete_all end - # rubocop: enable CodeReuse/ActiveRecord - - override :authorized_users - def authorized_users - User.from_union([ - group.project_users_with_descendants.select(:id), - group.members_with_parents.select(:user_id) - ], remove_duplicates: false) + + def todos + Todo.for_group(group.id) end - override :todos_to_remove? def todos_to_remove? group&.private? end diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb index e00d10c3780..a1ca0d8543c 100644 --- a/app/services/todos/destroy/project_private_service.rb +++ b/app/services/todos/destroy/project_private_service.rb @@ -7,27 +7,32 @@ module Todos attr_reader :project - # rubocop: disable CodeReuse/ActiveRecord def initialize(project_id) - @project = Project.find_by(id: project_id) + @project = Project.find_by_id(project_id) + end + + def execute + return unless todos_to_remove? + + delete_todos end - # rubocop: enable CodeReuse/ActiveRecord private - override :todos - # rubocop: disable CodeReuse/ActiveRecord + def delete_todos + authorized_users = ProjectAuthorization.select(:user_id).for_project(project_ids) + + todos.not_in_users(authorized_users).delete_all + end + def todos - Todo.where(project_id: project.id) + Todo.for_project(project.id) end - # rubocop: enable CodeReuse/ActiveRecord - override :project_ids def project_ids project.id end - override :todos_to_remove? def todos_to_remove? project&.private? end diff --git a/app/services/todos/destroy/unauthorized_features_service.rb b/app/services/todos/destroy/unauthorized_features_service.rb index 513def10575..22f7a0b2a37 100644 --- a/app/services/todos/destroy/unauthorized_features_service.rb +++ b/app/services/todos/destroy/unauthorized_features_service.rb @@ -27,6 +27,14 @@ module Todos private + def without_authorized(items) + items.not_in_users(authorized_users) + end + + def authorized_users + ProjectAuthorization.select(:user_id).for_project(project_ids) + end + def related_todos base_scope = Todo.for_project(project_id) base_scope = base_scope.for_user(user_id) if user_id diff --git a/app/services/update_container_registry_info_service.rb b/app/services/update_container_registry_info_service.rb index 7d79b257687..b720f9b14a5 100644 --- a/app/services/update_container_registry_info_service.rb +++ b/app/services/update_container_registry_info_service.rb @@ -11,7 +11,7 @@ class UpdateContainerRegistryInfoService # associated user when running this (e.g. from a rake task or a cron job), # so we need to generate a valid JWT token with no access permissions to # authenticate as a trusted client. - token = Auth::ContainerRegistryAuthenticationService.access_token([], []) + token = Auth::ContainerRegistryAuthenticationService.access_token({}) client = ContainerRegistry::Client.new(registry_config.api_url, token: token) info = client.registry_info @@ -24,7 +24,8 @@ class UpdateContainerRegistryInfoService Gitlab::CurrentSettings.update!( container_registry_vendor: info[:vendor] || '', container_registry_version: info[:version] || '', - container_registry_features: info[:features] || [] + container_registry_features: info[:features] || [], + container_registry_db_enabled: info[:db_enabled] || false ) end end diff --git a/app/services/users/allow_possible_spam_service.rb b/app/services/users/allow_possible_spam_service.rb deleted file mode 100644 index d9273fe0fc1..00000000000 --- a/app/services/users/allow_possible_spam_service.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Users - class AllowPossibleSpamService < BaseService - def initialize(current_user) - @current_user = current_user - end - - def execute(user) - custom_attribute = { - user_id: user.id, - key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM, - value: "#{current_user.username}/#{current_user.id}+#{Time.current}" - } - UserCustomAttribute.upsert_custom_attributes([custom_attribute]) - end - end -end diff --git a/app/services/users/auto_ban_service.rb b/app/services/users/auto_ban_service.rb new file mode 100644 index 00000000000..fa3b738b4cd --- /dev/null +++ b/app/services/users/auto_ban_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Users + class AutoBanService < BaseService + def initialize(user:, reason:) + @user = user + @reason = reason + end + + def execute + if user.ban + record_custom_attribute + success + else + messages = user.errors.full_messages + error(messages.uniq.join('. ')) + end + end + + private + + attr_reader :user, :reason + + def record_custom_attribute + custom_attribute = { + user_id: user.id, + key: UserCustomAttribute::AUTO_BANNED_BY, + value: reason + } + UserCustomAttribute.upsert_custom_attributes([custom_attribute]) + end + end +end diff --git a/app/services/users/in_product_marketing_email_records.rb b/app/services/users/in_product_marketing_email_records.rb index 94dbd809496..fcb252536b3 100644 --- a/app/services/users/in_product_marketing_email_records.rb +++ b/app/services/users/in_product_marketing_email_records.rb @@ -13,10 +13,9 @@ module Users @records = [] end - def add(user, campaign: nil, track: nil, series: nil) + def add(user, track: nil, series: nil) @records << Users::InProductMarketingEmail.new( user: user, - campaign: campaign, track: track, series: series, created_at: Time.zone.now, diff --git a/app/services/users/set_namespace_commit_email_service.rb b/app/services/users/set_namespace_commit_email_service.rb index 30ee597120d..775db364625 100644 --- a/app/services/users/set_namespace_commit_email_service.rb +++ b/app/services/users/set_namespace_commit_email_service.rb @@ -20,7 +20,7 @@ module Users return error(_("User doesn't exist or you don't have permission to change namespace commit emails.")) end - unless can?(target_user, :read_namespace, namespace) + unless can?(target_user, :read_namespace_via_membership, namespace) return error(_("Namespace doesn't exist or you don't have permission.")) end diff --git a/app/services/users/signup_service.rb b/app/services/users/signup_service.rb deleted file mode 100644 index 9eb1e75988c..00000000000 --- a/app/services/users/signup_service.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Users - class SignupService < BaseService - def initialize(current_user, params = {}) - @user = current_user - @params = params.dup - end - - def execute - assign_attributes - inject_validators - - if @user.save - ServiceResponse.success - else - ServiceResponse.error(message: @user.errors.full_messages.join('. ')) - end - end - - private - - def assign_attributes - @user.assign_attributes(params) unless params.empty? - end - - def inject_validators - class << @user - validates :role, presence: true - validates :setup_for_company, inclusion: { in: [true, false], message: :blank } if Gitlab.com? - end - end - end -end diff --git a/app/services/users/disallow_possible_spam_service.rb b/app/services/users/trust_service.rb index e31ba7ddff0..faf0b9c40ea 100644 --- a/app/services/users/disallow_possible_spam_service.rb +++ b/app/services/users/trust_service.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true module Users - class DisallowPossibleSpamService < BaseService + class TrustService < BaseService def initialize(current_user) @current_user = current_user end def execute(user) - user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).delete_all + UserCustomAttribute.set_trusted_by(user: user, trusted_by: @current_user) + success end end end diff --git a/app/services/users/untrust_service.rb b/app/services/users/untrust_service.rb new file mode 100644 index 00000000000..aa5de71b97f --- /dev/null +++ b/app/services/users/untrust_service.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Users + class UntrustService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + user.trusted_with_spam_attribute.delete + success + end + end +end diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb index 408ee429a74..59c73aa929c 100644 --- a/app/services/verify_pages_domain_service.rb +++ b/app/services/verify_pages_domain_service.rb @@ -46,9 +46,15 @@ class VerifyPagesDomainService < BaseService notify(:verification_succeeded) end + after_successful_verification + success end + def after_successful_verification + # method overridden in EE + end + def unverify_domain! was_verified = domain.verified? @@ -115,3 +121,5 @@ class VerifyPagesDomainService < BaseService notification_service.public_send("pages_domain_#{type}", domain) # rubocop:disable GitlabSecurity/PublicSend end end + +VerifyPagesDomainService.prepend_mod diff --git a/app/services/vs_code/settings/create_or_update_service.rb b/app/services/vs_code/settings/create_or_update_service.rb new file mode 100644 index 00000000000..27688b911b7 --- /dev/null +++ b/app/services/vs_code/settings/create_or_update_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module VsCode + module Settings + class CreateOrUpdateService + def initialize(current_user:, params: {}) + @current_user = current_user + @params = params + end + + def execute + # The GitLab VSCode settings API does not support creating or updating + # machines. + return ServiceResponse.success(payload: DEFAULT_MACHINE) if @params[:setting_type] == 'machines' + + setting = VsCodeSetting.by_user(current_user).by_setting_type(params[:setting_type]).first + + if setting.nil? + merged_params = params.merge(user: current_user, uuid: SecureRandom.uuid) + setting = VsCodeSetting.new(merged_params) + else + setting.content = params[:content] + setting.uuid = SecureRandom.uuid + end + + if setting.save + ServiceResponse.success(payload: setting) + else + ServiceResponse.error( + message: setting.errors.full_messages.to_sentence, + payload: { setting: setting } + ) + end + end + + private + + attr_reader :current_user, :params + end + end +end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 5bad2a1583c..27b29feed50 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -36,7 +36,9 @@ class WebHookService attr_accessor :hook, :data, :hook_name, :request_options attr_reader :uniqueness_token - def self.hook_to_event(hook_name) + def self.hook_to_event(hook_name, hook = nil) + return hook.class.name.titleize if hook.is_a?(SystemHook) + hook_name.to_s.singularize.titleize end @@ -194,7 +196,7 @@ class WebHookService headers = { 'Content-Type' => 'application/json', 'User-Agent' => "GitLab/#{Gitlab::VERSION}", - Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name), + Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name, hook), Gitlab::WebHooks::GITLAB_UUID_HEADER => SecureRandom.uuid, Gitlab::WebHooks::GITLAB_INSTANCE_HEADER => Gitlab.config.gitlab.base_url } diff --git a/app/services/work_items/related_work_item_links/create_service.rb b/app/services/work_items/related_work_item_links/create_service.rb index f313881470a..38e5ba3be7f 100644 --- a/app/services/work_items/related_work_item_links/create_service.rb +++ b/app/services/work_items/related_work_item_links/create_service.rb @@ -9,6 +9,7 @@ module WorkItems return error(_('No matching work item found.'), 404) unless can?(current_user, :admin_work_item_link, issuable) response = super + create_notes_async if new_links.any? if response[:status] == :success response[:message] = format( @@ -30,6 +31,10 @@ module WorkItems private + def create_notes(_issuable_link) + # no-op notes are created asynchronously + end + def link_class WorkItems::RelatedWorkItemLink end @@ -49,9 +54,23 @@ module WorkItems created_links.collect(&:target_id) end + def create_notes_async + link_ids = new_links.collect(&:id) + + worker_params = { + issuable_class: issuable.class.name, + issuable_id: issuable.id, + link_ids: link_ids, + link_type: params[:link_type] || 'relates_to', + user_id: current_user.id + } + + Issuable::RelatedLinksCreateWorker.perform_async(worker_params) + end + override :issuables_already_assigned_message def issuables_already_assigned_message - _('Work items are already linked') + _('Items are already linked') end override :issuables_not_found_message diff --git a/app/services/work_items/widgets/labels_service/update_service.rb b/app/services/work_items/widgets/labels_service/update_service.rb index b880398677d..b0791571924 100644 --- a/app/services/work_items/widgets/labels_service/update_service.rb +++ b/app/services/work_items/widgets/labels_service/update_service.rb @@ -11,6 +11,7 @@ module WorkItems end return if params.blank? + return unless has_permission?(:set_work_item_metadata) service_params.merge!(params.slice(:add_label_ids, :remove_label_ids)) end diff --git a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb index 0dbf3aa31d9..5d47b3a1516 100644 --- a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb +++ b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb @@ -8,6 +8,7 @@ module WorkItems return widget.work_item.assign_attributes({ start_date: nil, due_date: nil }) if new_type_excludes_widget? return if params.blank? + return unless has_permission?(:set_work_item_metadata) widget.work_item.assign_attributes(params.slice(:start_date, :due_date)) end |