summaryrefslogtreecommitdiff
path: root/app/services
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/services
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
downloadgitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/services')
-rw-r--r--app/services/alert_management/http_integrations/update_service.rb15
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb121
-rw-r--r--app/services/application_settings/update_service.rb2
-rw-r--r--app/services/authorized_project_update/recalculate_for_user_range_service.rb2
-rw-r--r--app/services/boards/create_service.rb8
-rw-r--r--app/services/boards/list_service.rb8
-rw-r--r--app/services/boards/lists/base_create_service.rb71
-rw-r--r--app/services/boards/lists/create_service.rb63
-rw-r--r--app/services/boards/visits/create_service.rb1
-rw-r--r--app/services/bulk_create_integration_service.rb10
-rw-r--r--app/services/bulk_import_service.rb2
-rw-r--r--app/services/captcha/captcha_verification_service.rb43
-rw-r--r--app/services/ci/create_job_artifacts_service.rb9
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/daily_build_group_report_result_service.rb3
-rw-r--r--app/services/ci/generate_codequality_mr_diff_report_service.rb30
-rw-r--r--app/services/ci/generate_coverage_reports_service.rb2
-rw-r--r--app/services/ci/pipeline_artifacts/coverage_report_service.rb2
-rw-r--r--app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb38
-rw-r--r--app/services/ci/pipeline_trigger_service.rb17
-rw-r--r--app/services/ci/process_build_service.rb2
-rw-r--r--app/services/ci/process_pipeline_service.rb13
-rw-r--r--app/services/ci/prometheus_metrics/observe_histograms_service.rb57
-rw-r--r--app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb4
-rw-r--r--app/services/clusters/kubernetes/configure_istio_ingress_service.rb2
-rw-r--r--app/services/concerns/alert_management/alert_processing.rb133
-rw-r--r--app/services/concerns/integrations/project_test_data.rb22
-rw-r--r--app/services/concerns/spam_check_methods.rb39
-rw-r--r--app/services/concerns/update_repository_storage_methods.rb19
-rw-r--r--app/services/container_expiration_policies/cleanup_service.rb15
-rw-r--r--app/services/deployments/create_service.rb8
-rw-r--r--app/services/design_management/move_designs_service.rb7
-rw-r--r--app/services/discussions/resolve_service.rb8
-rw-r--r--app/services/discussions/unresolve_service.rb21
-rw-r--r--app/services/draft_notes/publish_service.rb6
-rw-r--r--app/services/feature_flags/base_service.rb1
-rw-r--r--app/services/git/branch_hooks_service.rb44
-rw-r--r--app/services/git/wiki_push_service.rb9
-rw-r--r--app/services/groups/import_export/export_service.rb56
-rw-r--r--app/services/groups/import_export/import_service.rb52
-rw-r--r--app/services/groups/open_issues_count_service.rb64
-rw-r--r--app/services/issuable_base_service.rb29
-rw-r--r--app/services/issue_rebalancing_service.rb37
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb16
-rw-r--r--app/services/issues/update_service.rb17
-rw-r--r--app/services/jira/requests/base.rb8
-rw-r--r--app/services/members/update_service.rb6
-rw-r--r--app/services/merge_requests/add_context_service.rb3
-rw-r--r--app/services/merge_requests/approval_service.rb1
-rw-r--r--app/services/merge_requests/base_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb48
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb1
-rw-r--r--app/services/merge_requests/mark_reviewer_reviewed_service.rb19
-rw-r--r--app/services/merge_requests/merge_service.rb6
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb6
-rw-r--r--app/services/merge_requests/post_merge_service.rb31
-rw-r--r--app/services/merge_requests/reload_merge_head_diff_service.rb50
-rw-r--r--app/services/merge_requests/remove_approval_service.rb1
-rw-r--r--app/services/merge_requests/request_review_service.rb28
-rw-r--r--app/services/merge_requests/update_service.rb59
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb112
-rw-r--r--app/services/notes/create_service.rb13
-rw-r--r--app/services/notification_recipients/build_service.rb4
-rw-r--r--app/services/notification_recipients/builder/request_review.rb21
-rw-r--r--app/services/notification_service.rb12
-rw-r--r--app/services/packages/create_package_service.rb7
-rw-r--r--app/services/packages/debian/create_distribution_service.rb75
-rw-r--r--app/services/packages/debian/destroy_distribution_service.rb33
-rw-r--r--app/services/packages/debian/update_distribution_service.rb80
-rw-r--r--app/services/packages/generic/create_package_file_service.rb5
-rw-r--r--app/services/packages/maven/find_or_create_package_service.rb19
-rw-r--r--app/services/pages/delete_service.rb8
-rw-r--r--app/services/pages/migrate_from_legacy_storage_service.rb79
-rw-r--r--app/services/pages/migrate_legacy_storage_to_deployment_service.rb5
-rw-r--r--app/services/pages/zip_directory_service.rb53
-rw-r--r--app/services/projects/alerting/notify_service.rb94
-rw-r--r--app/services/projects/branches_by_mode_service.rb88
-rw-r--r--app/services/projects/cleanup_service.rb2
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb1
-rw-r--r--app/services/projects/create_service.rb20
-rw-r--r--app/services/projects/fork_service.rb11
-rw-r--r--app/services/projects/import_export/export_service.rb6
-rw-r--r--app/services/projects/update_pages_service.rb4
-rw-r--r--app/services/projects/update_service.rb18
-rw-r--r--app/services/quick_actions/interpret_service.rb9
-rw-r--r--app/services/repositories/changelog_service.rb115
-rw-r--r--app/services/repositories/housekeeping_service.rb2
-rw-r--r--app/services/resource_access_tokens/create_service.rb9
-rw-r--r--app/services/resource_access_tokens/revoke_service.rb8
-rw-r--r--app/services/resource_events/base_change_timebox_service.rb7
-rw-r--r--app/services/resource_events/change_milestone_service.rb4
-rw-r--r--app/services/security/ci_configuration/sast_create_service.rb66
-rw-r--r--app/services/security/ci_configuration/sast_parser_service.rb127
-rw-r--r--app/services/snippets/base_service.rb4
-rw-r--r--app/services/snippets/create_service.rb22
-rw-r--r--app/services/snippets/update_service.rb19
-rw-r--r--app/services/spam/spam_action_service.rb93
-rw-r--r--app/services/spam/spam_params.rb32
-rw-r--r--app/services/suggestions/apply_service.rb8
-rw-r--r--app/services/suggestions/create_service.rb2
-rw-r--r--app/services/system_hooks_service.rb36
-rw-r--r--app/services/system_note_service.rb11
-rw-r--r--app/services/system_notes/merge_requests_service.rb20
-rw-r--r--app/services/terraform/remote_state_handler.rb22
-rw-r--r--app/services/test_hooks/system_service.rb3
-rw-r--r--app/services/todo_service.rb8
-rw-r--r--app/services/users/approve_service.rb8
-rw-r--r--app/services/users/batch_status_cleaner_service.rb22
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb14
-rw-r--r--app/services/users/reject_service.rb14
111 files changed, 2242 insertions, 613 deletions
diff --git a/app/services/alert_management/http_integrations/update_service.rb b/app/services/alert_management/http_integrations/update_service.rb
index 220c4e759f0..af079f670b8 100644
--- a/app/services/alert_management/http_integrations/update_service.rb
+++ b/app/services/alert_management/http_integrations/update_service.rb
@@ -9,7 +9,7 @@ module AlertManagement
def initialize(integration, current_user, params)
@integration = integration
@current_user = current_user
- @params = params
+ @params = params.with_indifferent_access
end
def execute
@@ -17,7 +17,7 @@ module AlertManagement
params[:token] = nil if params.delete(:regenerate_token)
- if integration.update(params)
+ if integration.update(permitted_params)
success
else
error(integration.errors.full_messages.to_sentence)
@@ -32,6 +32,15 @@ module AlertManagement
current_user&.can?(:admin_operations, integration)
end
+ def permitted_params
+ params.slice(*permitted_params_keys)
+ end
+
+ # overriden in EE
+ def permitted_params_keys
+ %i[name active token]
+ end
+
def error(message)
ServiceResponse.error(message: message)
end
@@ -46,3 +55,5 @@ module AlertManagement
end
end
end
+
+::AlertManagement::HttpIntegrations::UpdateService.prepend_if_ee('::EE::AlertManagement::HttpIntegrations::UpdateService')
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index 753162bfdbf..0591376bcdf 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -2,9 +2,8 @@
module AlertManagement
class ProcessPrometheusAlertService
- include BaseServiceUtility
- include Gitlab::Utils::StrongMemoize
- include ::IncidentManagement::Settings
+ extend ::Gitlab::Utils::Override
+ include ::AlertManagement::AlertProcessing
def initialize(project, payload)
@project = project
@@ -14,11 +13,10 @@ module AlertManagement
def execute
return bad_request unless incoming_payload.has_required_attributes?
- process_alert_management_alert
+ process_alert
return bad_request unless alert.persisted?
- process_incident_issues if process_issues?
- send_alert_email if send_email?
+ complete_post_processing_tasks
ServiceResponse.success
end
@@ -27,110 +25,14 @@ module AlertManagement
attr_reader :project, :payload
- def process_alert_management_alert
- if incoming_payload.resolved?
- process_resolved_alert_management_alert
- else
- process_firing_alert_management_alert
- end
- end
-
- def process_firing_alert_management_alert
- if alert.persisted?
- alert.register_new_event!
- reset_alert_management_alert_status
- else
- create_alert_management_alert
- end
- end
-
- def reset_alert_management_alert_status
- return if alert.trigger
-
- logger.warn(
- message: 'Unable to update AlertManagement::Alert status to triggered',
- project_id: project.id,
- alert_id: alert.id
- )
- end
-
- def create_alert_management_alert
- if alert.save
- alert.execute_services
- SystemNoteService.create_new_alert(alert, Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus])
- return
- end
-
- logger.warn(
- message: 'Unable to create AlertManagement::Alert',
- project_id: project.id,
- alert_errors: alert.errors.messages
- )
- end
-
- def process_resolved_alert_management_alert
- return unless alert.persisted?
- return unless auto_close_incident?
-
- if alert.resolve(incoming_payload.ends_at)
- close_issue(alert.issue)
- return
- end
-
- logger.warn(
- message: 'Unable to update AlertManagement::Alert status to resolved',
- project_id: project.id,
- alert_id: alert.id
- )
- end
-
- def close_issue(issue)
- return if issue.blank? || issue.closed?
-
- Issues::CloseService
- .new(project, User.alert_bot)
- .execute(issue, system_note: false)
+ override :process_new_alert
+ def process_new_alert
+ return if resolving_alert?
- SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if issue.reset.closed?
- end
-
- def process_incident_issues
- return if alert.issue || alert.resolved?
-
- IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
- end
-
- def send_alert_email
- notification_service
- .async
- .prometheus_alerts_fired(project, [alert])
- end
-
- def logger
- @logger ||= Gitlab::AppLogger
- end
-
- def alert
- strong_memoize(:alert) do
- existing_alert || new_alert
- end
- end
-
- def existing_alert
- strong_memoize(:existing_alert) do
- AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first
- end
- end
-
- def new_alert
- strong_memoize(:new_alert) do
- AlertManagement::Alert.new(
- **incoming_payload.alert_params,
- ended_at: nil
- )
- end
+ super
end
+ override :incoming_payload
def incoming_payload
strong_memoize(:incoming_payload) do
Gitlab::AlertManagement::Payload.parse(
@@ -141,6 +43,11 @@ module AlertManagement
end
end
+ override :resolving_alert?
+ def resolving_alert?
+ incoming_payload.resolved?
+ end
+
def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 7792b811b4e..5e5c8ae2177 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -6,7 +6,7 @@ module ApplicationSettings
attr_reader :params, :application_setting
- MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist).freeze
+ MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_allowlist).freeze
def execute
result = update_settings
diff --git a/app/services/authorized_project_update/recalculate_for_user_range_service.rb b/app/services/authorized_project_update/recalculate_for_user_range_service.rb
index 14b0f5d6117..f300c45f019 100644
--- a/app/services/authorized_project_update/recalculate_for_user_range_service.rb
+++ b/app/services/authorized_project_update/recalculate_for_user_range_service.rb
@@ -9,7 +9,7 @@ module AuthorizedProjectUpdate
def execute
User.where(id: start_user_id..end_user_id).select(:id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord
- Users::RefreshAuthorizedProjectsService.new(user).execute
+ Users::RefreshAuthorizedProjectsService.new(user, source: self.class.name).execute
end
end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 2ccaea64d14..54dab581686 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -13,11 +13,11 @@ module Boards
private
def can_create_board?
- parent.boards.empty? || parent.multiple_issue_boards_available?
+ parent_board_collection.empty? || parent.multiple_issue_boards_available?
end
def create_board!
- board = parent.boards.create(params)
+ board = parent_board_collection.create(params)
unless board.persisted?
return ServiceResponse.error(message: "There was an error when creating a board.", payload: board)
@@ -30,6 +30,10 @@ module Boards
ServiceResponse.success(payload: board)
end
+
+ def parent_board_collection
+ parent.boards
+ end
end
end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index 729bca6580e..80ceb91f56d 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -2,9 +2,7 @@
module Boards
class ListService < Boards::BaseService
- def execute(create_default_board: true)
- create_board! if create_default_board && parent.boards.empty?
-
+ def execute
find_boards
end
@@ -18,10 +16,6 @@ module Boards
parent.boards.first_board
end
- def create_board!
- Boards::CreateService.new(parent, current_user).execute
- end
-
def find_boards
found =
if parent.multiple_issue_boards_available?
diff --git a/app/services/boards/lists/base_create_service.rb b/app/services/boards/lists/base_create_service.rb
new file mode 100644
index 00000000000..8399b1cc149
--- /dev/null
+++ b/app/services/boards/lists/base_create_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Boards
+ module Lists
+ # This class is used by issue and epic board lists
+ # for creating new list
+ class BaseCreateService < Boards::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute(board)
+ list = case type
+ when :backlog
+ create_backlog(board)
+ else
+ target = target(board)
+ position = next_position(board)
+
+ return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
+
+ create_list(board, type, target, position)
+ end
+
+ return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
+
+ ServiceResponse.success(payload: { list: list })
+ end
+
+ private
+
+ def type
+ # We don't ever expect to have more than one list
+ # type param at once.
+ if params.key?('backlog')
+ :backlog
+ else
+ :label
+ end
+ end
+
+ def target(board)
+ strong_memoize(:target) do
+ available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+
+ def available_labels
+ ::Labels::AvailableLabelsService.new(current_user, parent, {})
+ .available_labels
+ end
+
+ def next_position(board)
+ max_position = board.lists.movable.maximum(:position)
+ max_position.nil? ? 0 : max_position.succ
+ end
+
+ def create_list(board, type, target, position)
+ board.lists.create(create_list_attributes(type, target, position))
+ end
+
+ def create_list_attributes(type, target, position)
+ { type => target, list_type: type, position: position }
+ end
+
+ def create_backlog(board)
+ return board.lists.backlog.first if board.lists.backlog.exists?
+
+ board.lists.create(list_type: :backlog, position: nil)
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index a21ceee083f..37fe0a815bd 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -2,68 +2,7 @@
module Boards
module Lists
- class CreateService < Boards::BaseService
- include Gitlab::Utils::StrongMemoize
-
- def execute(board)
- list = case type
- when :backlog
- create_backlog(board)
- else
- target = target(board)
- position = next_position(board)
-
- return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
-
- create_list(board, type, target, position)
- end
-
- return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
-
- ServiceResponse.success(payload: { list: list })
- end
-
- private
-
- def type
- # We don't ever expect to have more than one list
- # type param at once.
- if params.key?('backlog')
- :backlog
- else
- :label
- end
- end
-
- def target(board)
- strong_memoize(:target) do
- available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
- end
- end
-
- def available_labels
- ::Labels::AvailableLabelsService.new(current_user, parent, {})
- .available_labels
- end
-
- def next_position(board)
- max_position = board.lists.movable.maximum(:position)
- max_position.nil? ? 0 : max_position.succ
- end
-
- def create_list(board, type, target, position)
- board.lists.create(create_list_attributes(type, target, position))
- end
-
- def create_list_attributes(type, target, position)
- { type => target, list_type: type, position: position }
- end
-
- def create_backlog(board)
- return board.lists.backlog.first if board.lists.backlog.exists?
-
- board.lists.create(list_type: :backlog, position: nil)
- end
+ class CreateService < Boards::Lists::BaseCreateService
end
end
end
diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb
index e2adf755511..428ed1a8bcc 100644
--- a/app/services/boards/visits/create_service.rb
+++ b/app/services/boards/visits/create_service.rb
@@ -5,6 +5,7 @@ module Boards
class CreateService < Boards::BaseService
def execute(board)
return unless current_user && Gitlab::Database.read_write?
+ return unless board.is_a?(Board) # other board types do not support board visits yet
if parent.is_a?(Group)
BoardGroupRecentVisit.visited!(current_user, board)
diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb
index df78c3645c7..ae756d0856e 100644
--- a/app/services/bulk_create_integration_service.rb
+++ b/app/services/bulk_create_integration_service.rb
@@ -11,8 +11,6 @@ class BulkCreateIntegrationService
service_list = ServiceList.new(batch, service_hash, association).to_array
Service.transaction do
- run_callbacks(batch) if association == 'project'
-
results = bulk_insert(*service_list)
if integration.data_fields_present?
@@ -33,14 +31,6 @@ class BulkCreateIntegrationService
klass.insert_all(items_to_insert, returning: [:id])
end
- # rubocop: disable CodeReuse/ActiveRecord
- def run_callbacks(batch)
- if integration.external_issue_tracker?
- Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def service_hash
if integration.template?
integration.to_service_hash
diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb
index bebf9153ce7..29439a79afe 100644
--- a/app/services/bulk_import_service.rb
+++ b/app/services/bulk_import_service.rb
@@ -38,6 +38,8 @@ class BulkImportService
bulk_import = create_bulk_import
BulkImportWorker.perform_async(bulk_import.id)
+
+ bulk_import
end
private
diff --git a/app/services/captcha/captcha_verification_service.rb b/app/services/captcha/captcha_verification_service.rb
new file mode 100644
index 00000000000..45a5a52367c
--- /dev/null
+++ b/app/services/captcha/captcha_verification_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Captcha
+ ##
+ # Encapsulates logic of checking captchas.
+ #
+ class CaptchaVerificationService
+ include Recaptcha::Verify
+
+ ##
+ # Performs verification of a captcha response.
+ #
+ # 'captcha_response' parameter is the response from the user solving a client-side captcha.
+ #
+ # 'request' parameter is the request which submitted the captcha.
+ #
+ # NOTE: Currently only supports reCAPTCHA, and is not yet used in all places of the app in which
+ # captchas are verified, but these can be addressed in future MRs. See:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/273480
+ def execute(captcha_response: nil, request:)
+ return false unless captcha_response
+
+ @request = request
+
+ Gitlab::Recaptcha.load_configurations!
+
+ # NOTE: We could pass the model and let the recaptcha gem automatically add errors to it,
+ # but we do not, for two reasons:
+ #
+ # 1. We want control over when the errors are added
+ # 2. We want control over the wording and i18n of the message
+ # 3. We want a consistent interface and behavior when adding support for other captcha
+ # libraries which may not support automatically adding errors to the model.
+ verify_recaptcha(response: captcha_response)
+ end
+
+ private
+
+ # The recaptcha library's Recaptcha::Verify#verify_recaptcha method requires that
+ # 'request' be a readable attribute - it doesn't support passing it as an options argument.
+ attr_reader :request
+ end
+end
diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb
index 5efb3805bf7..f1fdc8e2490 100644
--- a/app/services/ci/create_job_artifacts_service.rb
+++ b/app/services/ci/create_job_artifacts_service.rb
@@ -7,6 +7,7 @@ module Ci
ArtifactsExistError = Class.new(StandardError)
LSIF_ARTIFACT_TYPE = 'lsif'
+ METRICS_REPORT_UPLOAD_EVENT_NAME = 'i_testing_metrics_report_artifact_uploaders'
OBJECT_STORAGE_ERRORS = [
Errno::EIO,
@@ -42,6 +43,8 @@ module Ci
artifact, artifact_metadata = build_artifact(artifacts_file, params, metadata_file)
result = parse_artifact(artifact)
+ track_artifact_uploader(artifact)
+
return result unless result[:status] == :success
persist_artifact(artifact, artifact_metadata, params)
@@ -152,6 +155,12 @@ module Ci
)
end
+ def track_artifact_uploader(artifact)
+ return unless artifact.file_type == 'metrics'
+
+ track_usage_event(METRICS_REPORT_UPLOAD_EVENT_NAME, @job.user_id)
+ end
+
def parse_dotenv_artifact(artifact)
Ci::ParseDotenvArtifactService.new(project, current_user).execute(artifact)
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index d3001e54288..dc42411dfa1 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -123,7 +123,6 @@ module Ci
def record_conversion_event
Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates, current_user.id)
- Experiments::RecordConversionEventWorker.perform_async(:pipelines_empty_state, current_user.id)
end
def create_namespace_onboarding_action
diff --git a/app/services/ci/daily_build_group_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb
index bc966fb9634..820e6e08fc5 100644
--- a/app/services/ci/daily_build_group_report_result_service.rb
+++ b/app/services/ci/daily_build_group_report_result_service.rb
@@ -14,7 +14,8 @@ module Ci
ref_path: pipeline.source_ref_path,
date: pipeline.created_at.to_date,
last_pipeline_id: pipeline.id,
- default_branch: pipeline.default_branch?
+ default_branch: pipeline.default_branch?,
+ group_id: pipeline.project&.group&.id
}
aggregate(pipeline.builds.with_coverage).map do |group_name, group|
diff --git a/app/services/ci/generate_codequality_mr_diff_report_service.rb b/app/services/ci/generate_codequality_mr_diff_report_service.rb
new file mode 100644
index 00000000000..3b1bd319a4f
--- /dev/null
+++ b/app/services/ci/generate_codequality_mr_diff_report_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Ci
+ # TODO: a couple of points with this approach:
+ # + reuses existing architecture and reactive caching
+ # - it's not a report comparison and some comparing features must be turned off.
+ # see CompareReportsBaseService for more notes.
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+ class GenerateCodequalityMrDiffReportService < CompareReportsBaseService
+ def execute(base_pipeline, head_pipeline)
+ merge_request = MergeRequest.find_by_id(params[:id])
+ {
+ status: :parsed,
+ key: key(base_pipeline, head_pipeline),
+ data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request.new_paths)
+ }
+ rescue => e
+ Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
+ {
+ status: :error,
+ key: key(base_pipeline, head_pipeline),
+ status_reason: _('An error occurred while fetching codequality mr diff reports.')
+ }
+ end
+
+ def latest?(base_pipeline, head_pipeline, data)
+ data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
+ end
+ end
+end
diff --git a/app/services/ci/generate_coverage_reports_service.rb b/app/services/ci/generate_coverage_reports_service.rb
index 063fb966183..b3aa7b3091b 100644
--- a/app/services/ci/generate_coverage_reports_service.rb
+++ b/app/services/ci/generate_coverage_reports_service.rb
@@ -12,7 +12,7 @@ module Ci
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
- data: head_pipeline.pipeline_artifacts.find_with_code_coverage.present.for_files(merge_request.new_paths)
+ data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_coverage).present.for_files(merge_request.new_paths)
}
rescue => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
diff --git a/app/services/ci/pipeline_artifacts/coverage_report_service.rb b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
index 9f5c445c91a..8209639fa22 100644
--- a/app/services/ci/pipeline_artifacts/coverage_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
@@ -11,7 +11,7 @@ module Ci
pipeline.pipeline_artifacts.create!(
project_id: pipeline.project_id,
file_type: :code_coverage,
- file_format: :raw,
+ file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_coverage),
size: file["tempfile"].size,
file: file,
expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
diff --git a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
new file mode 100644
index 00000000000..5c52eef7ba6
--- /dev/null
+++ b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+module Ci
+ module PipelineArtifacts
+ class CreateCodeQualityMrDiffReportService
+ def execute(pipeline)
+ return unless pipeline.can_generate_codequality_reports?
+ return if pipeline.has_codequality_mr_diff_report?
+
+ file = build_carrierwave_file(pipeline)
+
+ pipeline.pipeline_artifacts.create!(
+ project_id: pipeline.project_id,
+ file_type: :code_quality_mr_diff,
+ file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_quality_mr_diff),
+ size: file["tempfile"].size,
+ file: file,
+ expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
+ )
+ end
+
+ private
+
+ def build_carrierwave_file(pipeline)
+ CarrierWaveStringFile.new_file(
+ file_content: build_quality_mr_diff_report(pipeline),
+ filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
+ content_type: 'application/json'
+ )
+ end
+
+ def build_quality_mr_diff_report(pipeline)
+ mr_diff_report = Gitlab::Ci::Reports::CodequalityMrDiff.new(pipeline.codequality_reports)
+
+ Ci::CodequalityMrDiffReportSerializer.new.represent(mr_diff_report).to_json # rubocop: disable CodeReuse/Serializer
+ end
+ end
+ end
+end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index a31f5e9056e..dbbaefb2b2f 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -17,6 +17,9 @@ module Ci
private
+ PAYLOAD_VARIABLE_KEY = 'TRIGGER_PAYLOAD'
+ PAYLOAD_VARIABLE_HIDDEN_PARAMS = %i(token).freeze
+
def create_pipeline_from_trigger(trigger)
# this check is to not leak the presence of the project if user cannot read it
return unless trigger.project == project
@@ -70,9 +73,23 @@ module Ci
end
def variables
+ if ::Feature.enabled?(:ci_trigger_payload_into_pipeline, project, default_enabled: :yaml)
+ param_variables + [payload_variable]
+ else
+ param_variables
+ end
+ end
+
+ def param_variables
params[:variables].to_h.map do |key, value|
{ key: key, value: value }
end
end
+
+ def payload_variable
+ { key: PAYLOAD_VARIABLE_KEY,
+ value: params.except(*PAYLOAD_VARIABLE_HIDDEN_PARAMS).to_json,
+ variable_type: :file }
+ end
end
end
diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb
index dd7b562cdb7..733aa75f255 100644
--- a/app/services/ci/process_build_service.rb
+++ b/app/services/ci/process_build_service.rb
@@ -26,7 +26,7 @@ module Ci
end
def valid_statuses_for_build(build)
- if ::Feature.enabled?(:skip_dag_manual_and_delayed_jobs, default_enabled: :yaml)
+ if ::Feature.enabled?(:skip_dag_manual_and_delayed_jobs, build.project, default_enabled: :yaml)
current_valid_statuses_for_build(build)
else
legacy_valid_statuses_for_build(build)
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index e511e26adfe..678b386fbbf 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -38,10 +38,15 @@ module Ci
# mark builds that are retried
if latest_statuses.any?
- pipeline.latest_statuses
- .where(name: latest_statuses.map(&:second))
- .where.not(id: latest_statuses.map(&:first))
- .update_all(retried: true)
+ updated_count = pipeline.latest_statuses
+ .where(name: latest_statuses.map(&:second))
+ .where.not(id: latest_statuses.map(&:first))
+ .update_all(retried: true)
+
+ # This counter is temporary. It will be used to check whether if we still use this method or not
+ # after setting correct value of `GenericCommitStatus#retried`.
+ # More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50465#note_491657115
+ metrics.legacy_update_jobs_counter.increment if updated_count > 0
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/ci/prometheus_metrics/observe_histograms_service.rb b/app/services/ci/prometheus_metrics/observe_histograms_service.rb
new file mode 100644
index 00000000000..527d87f19c2
--- /dev/null
+++ b/app/services/ci/prometheus_metrics/observe_histograms_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Ci
+ module PrometheusMetrics
+ class ObserveHistogramsService
+ class << self
+ def available_histograms
+ @available_histograms ||= [
+ histogram(:pipeline_graph_link_calculation_duration_seconds, 'Total time spent calculating links, in seconds', {}, [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.8, 1, 2]),
+ histogram(:pipeline_graph_links_total, 'Number of links per graph', {}, [1, 5, 10, 25, 50, 100, 200]),
+ histogram(:pipeline_graph_links_per_job_ratio, 'Ratio of links to job per graph', {}, [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1])
+ ].to_h
+ end
+
+ private
+
+ def histogram(name, *attrs)
+ [name.to_s, proc { Gitlab::Metrics.histogram(name, *attrs) }]
+ end
+ end
+
+ def initialize(project, params)
+ @project = project
+ @params = params
+ end
+
+ def execute
+ return ServiceResponse.success(http_status: :accepted) unless enabled?
+
+ params
+ .fetch(:histograms, [])
+ .each(&method(:observe))
+
+ ServiceResponse.success(http_status: :created)
+ end
+
+ private
+
+ attr_reader :project, :params
+
+ def observe(data)
+ histogram = find_histogram(data[:name])
+ histogram.observe({}, data[:value].to_f)
+ end
+
+ def find_histogram(name)
+ self.class.available_histograms
+ .fetch(name) { raise ActiveRecord::RecordNotFound }
+ .call
+ end
+
+ def enabled?
+ ::Feature.enabled?(:ci_accept_frontend_prometheus_metrics, project, default_enabled: :yaml)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
index a4bcca8e8b3..9e3e6de3928 100644
--- a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
+++ b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
@@ -7,8 +7,8 @@ module Ci
def execute(resource_group)
free_resources = resource_group.resources.free.count
- resource_group.builds.waiting_for_resource.take(free_resources).each do |build|
- build.enqueue_waiting_for_resource
+ resource_group.processables.waiting_for_resource.take(free_resources).each do |processable|
+ processable.enqueue_waiting_for_resource
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/clusters/kubernetes/configure_istio_ingress_service.rb b/app/services/clusters/kubernetes/configure_istio_ingress_service.rb
index 53c3c686f07..3b7e094bc97 100644
--- a/app/services/clusters/kubernetes/configure_istio_ingress_service.rb
+++ b/app/services/clusters/kubernetes/configure_istio_ingress_service.rb
@@ -60,7 +60,7 @@ module Clusters
cert.public_key = key.public_key
cert.subject = name
cert.issuer = name
- cert.sign(key, OpenSSL::Digest::SHA256.new)
+ cert.sign(key, OpenSSL::Digest.new('SHA256'))
serverless_domain_cluster.update!(
key: key.to_pem,
diff --git a/app/services/concerns/alert_management/alert_processing.rb b/app/services/concerns/alert_management/alert_processing.rb
new file mode 100644
index 00000000000..9b15c5d7b4b
--- /dev/null
+++ b/app/services/concerns/alert_management/alert_processing.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ # Module to support the processing of new alert payloads
+ # from various sources. Payloads may be for new alerts,
+ # existing alerts, or acting as a resolving alert.
+ #
+ # Performs processing-related tasks, such as creating system
+ # notes, creating or resolving related issues, and notifying
+ # stakeholders of the alert.
+ #
+ # Requires #project [Project] and #payload [Hash] methods
+ # to be defined.
+ module AlertProcessing
+ include BaseServiceUtility
+ include Gitlab::Utils::StrongMemoize
+ include ::IncidentManagement::Settings
+
+ # Updates or creates alert from payload for project
+ # including system notes
+ def process_alert
+ if alert.persisted?
+ process_existing_alert
+ else
+ process_new_alert
+ end
+ end
+
+ # Creates or closes issue for alert and notifies stakeholders
+ def complete_post_processing_tasks
+ process_incident_issues if process_issues?
+ send_alert_email if send_email? && notifying_alert?
+ end
+
+ def process_existing_alert
+ if resolving_alert?
+ process_resolved_alert
+ else
+ process_firing_alert
+ end
+ end
+
+ def process_resolved_alert
+ return unless auto_close_incident?
+ return close_issue(alert.issue) if alert.resolve(incoming_payload.ends_at)
+
+ logger.warn(
+ message: 'Unable to update AlertManagement::Alert status to resolved',
+ project_id: project.id,
+ alert_id: alert.id
+ )
+ end
+
+ def process_firing_alert
+ alert.register_new_event!
+ end
+
+ def close_issue(issue)
+ return if issue.blank? || issue.closed?
+
+ ::Issues::CloseService
+ .new(project, User.alert_bot)
+ .execute(issue, system_note: false)
+
+ SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if issue.reset.closed?
+ end
+
+ def process_new_alert
+ if alert.save
+ alert.execute_services
+ SystemNoteService.create_new_alert(alert, alert_source)
+ else
+ logger.warn(
+ message: "Unable to create AlertManagement::Alert from #{alert_source}",
+ project_id: project.id,
+ alert_errors: alert.errors.messages
+ )
+ end
+ end
+
+ def process_incident_issues
+ return if alert.issue || alert.resolved?
+
+ ::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
+ end
+
+ def send_alert_email
+ notification_service
+ .async
+ .prometheus_alerts_fired(project, [alert])
+ end
+
+ def incoming_payload
+ strong_memoize(:incoming_payload) do
+ Gitlab::AlertManagement::Payload.parse(project, payload.to_h, integration: integration)
+ end
+ end
+
+ def alert
+ strong_memoize(:alert) do
+ find_existing_alert || build_new_alert
+ end
+ end
+
+ def find_existing_alert
+ return unless incoming_payload.gitlab_fingerprint
+
+ AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first
+ end
+
+ def build_new_alert
+ AlertManagement::Alert.new(**incoming_payload.alert_params, ended_at: nil)
+ end
+
+ def resolving_alert?
+ incoming_payload.ends_at.present?
+ end
+
+ def notifying_alert?
+ alert.triggered? || alert.resolved?
+ end
+
+ def alert_source
+ alert.monitoring_tool
+ end
+
+ def logger
+ @logger ||= Gitlab::AppLogger
+ end
+ end
+end
+
+AlertManagement::AlertProcessing.prepend_ee_mod
diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb
index 72c12cfb394..57bcba98b49 100644
--- a/app/services/concerns/integrations/project_test_data.rb
+++ b/app/services/concerns/integrations/project_test_data.rb
@@ -9,35 +9,40 @@ module Integrations
end
def note_events_data
- note = project.notes.first
+ note = NotesFinder.new(current_user, project: project, target: project).execute.reorder(nil).last # rubocop: disable CodeReuse/ActiveRecord
+
return { error: s_('TestHooks|Ensure the project has notes.') } unless note.present?
Gitlab::DataBuilder::Note.build(note, current_user)
end
def issues_events_data
- issue = project.issues.first
+ issue = IssuesFinder.new(current_user, project_id: project.id, sort: 'created_desc').execute.first
+
return { error: s_('TestHooks|Ensure the project has issues.') } unless issue.present?
issue.to_hook_data(current_user)
end
def merge_requests_events_data
- merge_request = project.merge_requests.first
+ merge_request = MergeRequestsFinder.new(current_user, project_id: project.id, sort: 'created_desc').execute.first
+
return { error: s_('TestHooks|Ensure the project has merge requests.') } unless merge_request.present?
merge_request.to_hook_data(current_user)
end
def job_events_data
- build = project.builds.first
+ build = Ci::JobsFinder.new(current_user: current_user, project: project).execute.first
+
return { error: s_('TestHooks|Ensure the project has CI jobs.') } unless build.present?
Gitlab::DataBuilder::Build.build(build)
end
def pipeline_events_data
- pipeline = project.ci_pipelines.newest_first.first
+ pipeline = Ci::PipelinesFinder.new(project, current_user, order_by: 'id', sort: 'desc').execute.first
+
return { error: s_('TestHooks|Ensure the project has CI pipelines.') } unless pipeline.present?
Gitlab::DataBuilder::Pipeline.build(pipeline)
@@ -45,6 +50,7 @@ module Integrations
def wiki_page_events_data
page = project.wiki.list_pages(limit: 1).first
+
if !project.wiki_enabled? || page.blank?
return { error: s_('TestHooks|Ensure the wiki is enabled and has pages.') }
end
@@ -53,14 +59,16 @@ module Integrations
end
def deployment_events_data
- deployment = project.deployments.first
+ deployment = DeploymentsFinder.new(project: project, order_by: 'created_at', sort: 'desc').execute.first
+
return { error: s_('TestHooks|Ensure the project has deployments.') } unless deployment.present?
Gitlab::DataBuilder::Deployment.build(deployment)
end
def releases_events_data
- release = project.releases.first
+ release = ReleasesFinder.new(project, current_user, order_by: :created_at, sort: :desc).execute.first
+
return { error: s_('TestHooks|Ensure the project has releases.') } unless release.present?
release.to_hook_data('create')
diff --git a/app/services/concerns/spam_check_methods.rb b/app/services/concerns/spam_check_methods.rb
deleted file mode 100644
index 939f8f183ab..00000000000
--- a/app/services/concerns/spam_check_methods.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-# SpamCheckMethods
-#
-# Provide helper methods for checking if a given spammable object has
-# potential spam data.
-#
-# Dependencies:
-# - params with :request
-
-module SpamCheckMethods
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def filter_spam_check_params
- @request = params.delete(:request)
- @api = params.delete(:api)
- @recaptcha_verified = params.delete(:recaptcha_verified)
- @spam_log_id = params.delete(:spam_log_id)
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
-
- # In order to be proceed to the spam check process, @spammable has to be
- # a dirty instance, which means it should be already assigned with the new
- # attribute values.
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def spam_check(spammable, user, action:)
- raise ArgumentError.new('Please provide an action, such as :create') unless action
-
- Spam::SpamActionService.new(
- spammable: spammable,
- request: @request,
- user: user,
- context: { action: action }
- ).execute(
- api: @api,
- recaptcha_verified: @recaptcha_verified,
- spam_log_id: @spam_log_id)
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
-end
diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb
index c3a55e9379e..6e4824bd784 100644
--- a/app/services/concerns/update_repository_storage_methods.rb
+++ b/app/services/concerns/update_repository_storage_methods.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
module UpdateRepositoryStorageMethods
+ include Gitlab::Utils::StrongMemoize
+
Error = Class.new(StandardError)
- SameFilesystemError = Class.new(Error)
attr_reader :repository_storage_move
delegate :container, :source_storage_name, :destination_storage_name, to: :repository_storage_move
@@ -18,9 +19,7 @@ module UpdateRepositoryStorageMethods
repository_storage_move.start!
end
- raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name)
-
- mirror_repositories
+ mirror_repositories unless same_filesystem?
repository_storage_move.transaction do
repository_storage_move.finish_replication!
@@ -28,8 +27,10 @@ module UpdateRepositoryStorageMethods
track_repository(destination_storage_name)
end
- remove_old_paths
- enqueue_housekeeping
+ unless same_filesystem?
+ remove_old_paths
+ enqueue_housekeeping
+ end
repository_storage_move.finish_cleanup!
@@ -80,8 +81,10 @@ module UpdateRepositoryStorageMethods
end
end
- def same_filesystem?(old_storage, new_storage)
- Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage)
+ def same_filesystem?
+ strong_memoize(:same_filesystem) do
+ Gitlab::GitalyClient.filesystem_id(source_storage_name) == Gitlab::GitalyClient.filesystem_id(destination_storage_name)
+ end
end
def remove_old_paths
diff --git a/app/services/container_expiration_policies/cleanup_service.rb b/app/services/container_expiration_policies/cleanup_service.rb
index b9e623e2e07..69e5620d986 100644
--- a/app/services/container_expiration_policies/cleanup_service.rb
+++ b/app/services/container_expiration_policies/cleanup_service.rb
@@ -4,7 +4,7 @@ module ContainerExpirationPolicies
class CleanupService
attr_reader :repository
- SERVICE_RESULT_FIELDS = %i[original_size before_truncate_size after_truncate_size before_delete_size].freeze
+ SERVICE_RESULT_FIELDS = %i[original_size before_truncate_size after_truncate_size before_delete_size deleted_size].freeze
def initialize(repository)
@repository = repository
@@ -15,9 +15,15 @@ module ContainerExpirationPolicies
repository.start_expiration_policy!
- service_result = Projects::ContainerRepository::CleanupTagsService
- .new(project, nil, policy_params.merge('container_expiration_policy' => true))
- .execute(repository)
+ begin
+ service_result = Projects::ContainerRepository::CleanupTagsService
+ .new(project, nil, policy_params.merge('container_expiration_policy' => true))
+ .execute(repository)
+ rescue
+ repository.cleanup_unfinished!
+
+ raise
+ end
if service_result[:status] == :success
repository.update!(
@@ -25,6 +31,7 @@ module ContainerExpirationPolicies
expiration_policy_started_at: nil,
expiration_policy_completed_at: Time.zone.now
)
+
success(:finished, service_result)
else
repository.cleanup_unfinished!
diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb
index 7355747d778..ebf2b077bca 100644
--- a/app/services/deployments/create_service.rb
+++ b/app/services/deployments/create_service.rb
@@ -11,6 +11,8 @@ module Deployments
end
def execute
+ return last_deployment if last_deployment&.equal_to?(params)
+
environment.deployments.build(deployment_attributes).tap do |deployment|
# Deployment#change_status already saves the model, so we only need to
# call #save ourselves if no status is provided.
@@ -36,5 +38,11 @@ module Deployments
on_stop: params[:on_stop]
}
end
+
+ private
+
+ def last_deployment
+ @environment.last_deployment
+ end
end
end
diff --git a/app/services/design_management/move_designs_service.rb b/app/services/design_management/move_designs_service.rb
index ca715b10351..129f93edf5e 100644
--- a/app/services/design_management/move_designs_service.rb
+++ b/app/services/design_management/move_designs_service.rb
@@ -16,7 +16,6 @@ module DesignManagement
return error(:cannot_move) unless current_user.can?(:move_design, current_design)
return error(:no_neighbors) unless neighbors.present?
return error(:not_distinct) unless all_distinct?
- return error(:not_adjacent) if any_in_gap?
return error(:not_same_issue) unless all_same_issue?
move_nulls_to_end
@@ -54,12 +53,6 @@ module DesignManagement
ids.uniq.size == ids.size
end
- def any_in_gap?
- return false unless previous_design&.relative_position && next_design&.relative_position
-
- !previous_design.immediately_before?(next_design)
- end
-
def all_same_issue?
issue.designs.id_in(ids).count == ids.size
end
diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb
index cd5925cd9be..91c3cf136a4 100644
--- a/app/services/discussions/resolve_service.rb
+++ b/app/services/discussions/resolve_service.rb
@@ -40,7 +40,13 @@ module Discussions
discussion.resolve!(current_user)
@resolved_count += 1
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request) if merge_request
+ if merge_request
+ Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
+ .track_resolve_thread_action(user: current_user)
+
+ MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ end
+
SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue
end
diff --git a/app/services/discussions/unresolve_service.rb b/app/services/discussions/unresolve_service.rb
new file mode 100644
index 00000000000..fbd96ceafe7
--- /dev/null
+++ b/app/services/discussions/unresolve_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Discussions
+ class UnresolveService < Discussions::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(discussion, user)
+ @discussion = discussion
+ @user = user
+
+ super
+ end
+
+ def execute
+ @discussion.unresolve!
+
+ Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
+ .track_unresolve_thread_action(user: @user)
+ end
+ end
+end
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index 316abff4552..82917241347 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -38,6 +38,8 @@ module DraftNotes
end
draft_notes.delete_all
+ set_reviewed
+
notification_service.async.new_review(review)
MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
end
@@ -64,5 +66,9 @@ module DraftNotes
discussion.unresolve!
end
end
+
+ def set_reviewed
+ ::MergeRequests::MarkReviewerReviewedService.new(project, current_user).execute(merge_request)
+ end
end
end
diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb
index c11c465252e..f48f95e2550 100644
--- a/app/services/feature_flags/base_service.rb
+++ b/app/services/feature_flags/base_service.rb
@@ -41,7 +41,6 @@ module FeatureFlags
def sync_to_jira(feature_flag)
return unless feature_flag.present?
- return unless Feature.enabled?(:jira_sync_feature_flags, feature_flag.project)
seq_id = ::Atlassian::JiraConnect::Client.generate_update_sequence_id
feature_flag.run_after_commit do
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 4edcff0e3d0..825faf59c13 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -2,6 +2,8 @@
module Git
class BranchHooksService < ::Git::BaseHooksService
+ extend ::Gitlab::Utils::Override
+
def execute
execute_branch_hooks
@@ -41,14 +43,15 @@ module Git
super
end
+ override :invalidated_file_types
def invalidated_file_types
return super unless default_branch? && !creating_branch?
- paths = limited_commits.each_with_object(Set.new) do |commit, set|
- commit.raw_deltas.each do |diff|
- set << diff.new_path
- end
- end
+ modified_file_types
+ end
+
+ def modified_file_types
+ paths = commit_paths.values.reduce(&:merge) || Set.new
Gitlab::FileDetector.types_in_paths(paths)
end
@@ -77,6 +80,7 @@ module Git
enqueue_process_commit_messages
enqueue_jira_connect_sync_messages
enqueue_metrics_dashboard_sync
+ track_ci_config_change_event
end
def branch_remove_hooks
@@ -85,10 +89,23 @@ module Git
def enqueue_metrics_dashboard_sync
return unless default_branch?
+ return unless modified_file_types.include?(:metrics_dashboard)
::Metrics::Dashboard::SyncDashboardsWorker.perform_async(project.id)
end
+ def track_ci_config_change_event
+ return unless Gitlab::CurrentSettings.usage_ping_enabled?
+ return unless ::Feature.enabled?(:usage_data_unique_users_committing_ciconfigfile, project, default_enabled: :yaml)
+ return unless default_branch?
+
+ commits_changing_ci_config.each do |commit|
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(
+ 'o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit.author&.id
+ )
+ end
+ end
+
# Schedules processing of commit messages
def enqueue_process_commit_messages
referencing_commits = limited_commits.select(&:matches_cross_reference_regex?)
@@ -190,6 +207,23 @@ module Git
set
end
+
+ def commits_changing_ci_config
+ commit_paths.select do |commit, paths|
+ next if commit.merge_commit?
+
+ paths.include?(project.ci_config_path_or_default)
+ end.keys
+ end
+
+ def commit_paths
+ strong_memoize(:commit_paths) do
+ limited_commits.to_h do |commit|
+ paths = Set.new(commit.raw_deltas.map(&:new_path))
+ [commit, paths]
+ end
+ end
+ end
end
end
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index 87e2be858c0..0905b2d98df 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -16,6 +16,7 @@ module Git
wiki.after_post_receive
process_changes
+ perform_housekeeping
end
private
@@ -72,6 +73,14 @@ module Git
def default_branch_changes
@default_branch_changes ||= changes.select { |change| on_default_branch?(change) }
end
+
+ def perform_housekeeping
+ housekeeping = Repositories::HousekeepingService.new(wiki)
+ housekeeping.increment!
+ housekeeping.execute if housekeeping.needed?
+ rescue Repositories::HousekeepingService::LeaseTaken
+ # no-op
+ end
end
end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index abac0ffc5d9..a436aec1b39 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -12,40 +12,44 @@ module Groups
end
def async_execute
- GroupExportWorker.perform_async(@current_user.id, @group.id, @params)
+ GroupExportWorker.perform_async(current_user.id, group.id, params)
end
def execute
validate_user_permissions
- remove_existing_export! if @group.export_file_exists?
+ remove_existing_export! if group.export_file_exists?
save!
ensure
- remove_base_tmp_dir
+ remove_archive_tmp_dir
end
private
+ attr_reader :group, :current_user, :params
attr_accessor :shared
def validate_user_permissions
- unless @current_user.can?(:admin_group, @group)
- @shared.error(::Gitlab::ImportExport::Error.permission_error(@current_user, @group))
+ unless current_user.can?(:admin_group, group)
+ shared.error(::Gitlab::ImportExport::Error.permission_error(current_user, group))
notify_error!
end
end
def remove_existing_export!
- import_export_upload = @group.import_export_upload
+ import_export_upload = group.import_export_upload
import_export_upload.remove_export_file!
import_export_upload.save
end
def save!
- if savers.all?(&:save)
+ # We cannot include the file_saver with the other savers because
+ # it removes the tmp dir. This means that if we want to add new savers
+ # in EE the data won't be available.
+ if savers.all?(&:save) && file_saver.save
notify_success
else
notify_error!
@@ -53,36 +57,40 @@ module Groups
end
def savers
- [version_saver, tree_exporter, file_saver]
+ [version_saver, tree_exporter]
end
def tree_exporter
tree_exporter_class.new(
- group: @group,
- current_user: @current_user,
- shared: @shared,
- params: @params
+ group: group,
+ current_user: current_user,
+ shared: shared,
+ params: params
)
end
def tree_exporter_class
- if ::Feature.enabled?(:group_export_ndjson, @group&.parent, default_enabled: true)
+ if ndjson?
Gitlab::ImportExport::Group::TreeSaver
else
Gitlab::ImportExport::Group::LegacyTreeSaver
end
end
+ def ndjson?
+ ::Feature.enabled?(:group_export_ndjson, group&.parent, default_enabled: :yaml)
+ end
+
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: shared)
end
def file_saver
- Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
+ Gitlab::ImportExport::Saver.new(exportable: group, shared: shared)
end
- def remove_base_tmp_dir
- FileUtils.rm_rf(shared.base_path) if shared&.base_path
+ def remove_archive_tmp_dir
+ FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
end
def notify_error!
@@ -94,22 +102,22 @@ module Groups
def notify_success
@logger.info(
message: 'Group Export succeeded',
- group_id: @group.id,
- group_name: @group.name
+ group_id: group.id,
+ group_name: group.name
)
- notification_service.group_was_exported(@group, @current_user)
+ notification_service.group_was_exported(group, current_user)
end
def notify_error
@logger.error(
message: 'Group Export failed',
- group_id: @group.id,
- group_name: @group.name,
- errors: @shared.errors.join(', ')
+ group_id: group.id,
+ group_name: group.name,
+ errors: shared.errors.join(', ')
)
- notification_service.group_was_not_exported(@group, @current_user, @shared.errors)
+ notification_service.group_was_not_exported(group, current_user, shared.errors)
end
def notification_service
@@ -118,3 +126,5 @@ module Groups
end
end
end
+
+Groups::ImportExport::ExportService.prepend_if_ee('EE::Groups::ImportExport::ExportService')
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index a0ddc50e5e0..bf3f09f22d4 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -3,7 +3,7 @@
module Groups
module ImportExport
class ImportService
- attr_reader :current_user, :group, :params
+ attr_reader :current_user, :group, :shared
def initialize(group:, user:)
@group = group
@@ -26,10 +26,10 @@ module Groups
end
def execute
- if valid_user_permissions? && import_file && restorer.restore
+ if valid_user_permissions? && import_file && restorers.all?(&:restore)
notify_success
- @group
+ group
else
notify_error!
end
@@ -43,37 +43,41 @@ module Groups
def import_file
@import_file ||= Gitlab::ImportExport::FileImporter.import(
- importable: @group,
+ importable: group,
archive_file: nil,
- shared: @shared
+ shared: shared
)
end
- def restorer
- @restorer ||=
+ def restorers
+ [tree_restorer]
+ end
+
+ def tree_restorer
+ @tree_restorer ||=
if ndjson?
Gitlab::ImportExport::Group::TreeRestorer.new(
- user: @current_user,
- shared: @shared,
- group: @group
+ user: current_user,
+ shared: shared,
+ group: group
)
else
Gitlab::ImportExport::Group::LegacyTreeRestorer.new(
- user: @current_user,
- shared: @shared,
- group: @group,
+ user: current_user,
+ shared: shared,
+ group: group,
group_hash: nil
)
end
end
def ndjson?
- ::Feature.enabled?(:group_import_ndjson, @group&.parent, default_enabled: true) &&
- File.exist?(File.join(@shared.export_path, 'tree/groups/_all.ndjson'))
+ ::Feature.enabled?(:group_import_ndjson, group&.parent, default_enabled: true) &&
+ File.exist?(File.join(shared.export_path, 'tree/groups/_all.ndjson'))
end
def remove_import_file
- upload = @group.import_export_upload
+ upload = group.import_export_upload
return unless upload&.import_file&.file
@@ -85,7 +89,7 @@ module Groups
if current_user.can?(:admin_group, group)
true
else
- @shared.error(::Gitlab::ImportExport::Error.permission_error(current_user, group))
+ shared.error(::Gitlab::ImportExport::Error.permission_error(current_user, group))
false
end
@@ -93,16 +97,16 @@ module Groups
def notify_success
@logger.info(
- group_id: @group.id,
- group_name: @group.name,
+ group_id: group.id,
+ group_name: group.name,
message: 'Group Import/Export: Import succeeded'
)
end
def notify_error
@logger.error(
- group_id: @group.id,
- group_name: @group.name,
+ group_id: group.id,
+ group_name: group.name,
message: "Group Import/Export: Errors occurred, see '#{Gitlab::ErrorTracking::Logger.file_name}' for details"
)
end
@@ -110,12 +114,14 @@ module Groups
def notify_error!
notify_error
- raise Gitlab::ImportExport::Error.new(@shared.errors.to_sentence)
+ raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
end
def remove_base_tmp_dir
- FileUtils.rm_rf(@shared.base_path)
+ FileUtils.rm_rf(shared.base_path)
end
end
end
end
+
+Groups::ImportExport::ImportService.prepend_if_ee('EE::Groups::ImportExport::ImportService')
diff --git a/app/services/groups/open_issues_count_service.rb b/app/services/groups/open_issues_count_service.rb
new file mode 100644
index 00000000000..db1ca09212a
--- /dev/null
+++ b/app/services/groups/open_issues_count_service.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Groups
+ # Service class for counting and caching the number of open issues of a group.
+ class OpenIssuesCountService < BaseCountService
+ include Gitlab::Utils::StrongMemoize
+
+ VERSION = 1
+ PUBLIC_COUNT_KEY = 'group_public_open_issues_count'
+ TOTAL_COUNT_KEY = 'group_total_open_issues_count'
+ CACHED_COUNT_THRESHOLD = 1000
+ EXPIRATION_TIME = 24.hours
+
+ attr_reader :group, :user
+
+ def initialize(group, user = nil)
+ @group = group
+ @user = user
+ end
+
+ # Reads count value from cache and return it if present.
+ # If empty or expired, #uncached_count will calculate the issues count for the group and
+ # compare it with the threshold. If it is greater, it will be written to the cache and returned.
+ # If below, it will be returned without being cached.
+ # This results in only caching large counts and calculating the rest with every call to maintain
+ # accuracy.
+ def count
+ cached_count = Rails.cache.read(cache_key)
+ return cached_count unless cached_count.blank?
+
+ refreshed_count = uncached_count
+ update_cache_for_key(cache_key) { refreshed_count } if refreshed_count > CACHED_COUNT_THRESHOLD
+ refreshed_count
+ end
+
+ def cache_key(key = nil)
+ ['groups', 'open_issues_count_service', VERSION, group.id, cache_key_name]
+ end
+
+ private
+
+ def cache_options
+ super.merge({ expires_in: EXPIRATION_TIME })
+ end
+
+ def cache_key_name
+ public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
+ end
+
+ def public_only?
+ !user_is_at_least_reporter?
+ end
+
+ def user_is_at_least_reporter?
+ strong_memoize(:user_is_at_least_reporter) do
+ group.member?(user, Gitlab::Access::REPORTER)
+ end
+ end
+
+ def relation_for_count
+ IssuesFinder.new(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: public_only?).execute
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 6d41d449683..094b31b4ad6 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -190,11 +190,7 @@ class IssuableBaseService < BaseService
change_additional_attributes(issuable)
old_associations = associations_before_update(issuable)
- label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
- if labels_changing?(issuable.label_ids, label_ids)
- params[:label_ids] = label_ids
- issuable.touch
- end
+ assign_requested_labels(issuable)
if issuable.changed? || params.present?
issuable.assign_attributes(params)
@@ -262,6 +258,11 @@ class IssuableBaseService < BaseService
invalidate_cache_counts(issuable, users: issuable.assignees.to_a)
after_update(issuable)
execute_hooks(issuable, 'update', old_associations: nil)
+
+ if issuable.is_a?(MergeRequest)
+ Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
+ .track_task_item_status_changed(user: current_user)
+ end
end
end
@@ -297,10 +298,6 @@ class IssuableBaseService < BaseService
update_task(issuable)
end
- def labels_changing?(old_label_ids, new_label_ids)
- old_label_ids.sort != new_label_ids.sort
- end
-
def has_title_or_description_changed?(issuable)
issuable.title_changed? || issuable.description_changed?
end
@@ -349,6 +346,20 @@ class IssuableBaseService < BaseService
end
# rubocop: enable CodeReuse/ActiveRecord
+ def assign_requested_labels(issuable)
+ label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ return unless ids_changing?(issuable.label_ids, label_ids)
+
+ params[:label_ids] = label_ids
+ issuable.touch
+ end
+
+ # Arrays of ids are used, but we should really use sets of ids, so
+ # let's have an helper to properly check if some ids are changing
+ def ids_changing?(old_array, new_array)
+ old_array.sort != new_array.sort
+ end
+
def toggle_award(issuable)
award = params.delete(:emoji_award)
AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
diff --git a/app/services/issue_rebalancing_service.rb b/app/services/issue_rebalancing_service.rb
index 4138c6441c8..849afc4edb8 100644
--- a/app/services/issue_rebalancing_service.rb
+++ b/app/services/issue_rebalancing_service.rb
@@ -17,8 +17,21 @@ class IssueRebalancingService
start = RelativePositioning::START_POSITION - (gaps / 2) * gap_size
- Issue.transaction do
- indexed_ids.each_slice(100) { |pairs| assign_positions(start, pairs) }
+ if Feature.enabled?(:issue_rebalancing_optimization)
+ Issue.transaction do
+ assign_positions(start, indexed_ids)
+ .sort_by(&:first)
+ .each_slice(100) do |pairs_with_position|
+ update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id')
+ end
+ end
+ else
+ Issue.transaction do
+ indexed_ids.each_slice(100) do |pairs|
+ pairs_with_position = assign_positions(start, pairs)
+ update_positions(pairs_with_position, 'rebalance issue positions')
+ end
+ end
end
end
@@ -32,13 +45,22 @@ class IssueRebalancingService
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def assign_positions(start, positions)
- values = positions.map do |id, index|
- "(#{id}, #{start + (index * gap_size)})"
+ def assign_positions(start, pairs)
+ pairs.map do |id, index|
+ [id, start + (index * gap_size)]
+ end
+ end
+
+ def update_positions(pairs_with_position, query_name)
+ values = pairs_with_position.map do |id, index|
+ "(#{id}, #{index})"
end.join(', ')
- Issue.connection.exec_query(<<~SQL, "rebalance issue positions")
+ run_update_query(values, query_name)
+ end
+
+ def run_update_query(values, query_name)
+ Issue.connection.exec_query(<<~SQL, query_name)
WITH cte(cte_id, new_pos) AS (
SELECT *
FROM (VALUES #{values}) as t (id, pos)
@@ -49,7 +71,6 @@ class IssueRebalancingService
WHERE cte_id = id
SQL
end
- # rubocop: enable CodeReuse/ActiveRecord
def issue_count
@issue_count ||= base.count
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index baf7974c45d..746f7d1f4c1 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -55,7 +55,7 @@ module Issues
def close_external_issue(issue, closed_via)
return unless project.external_issue_tracker&.support_close_issue?
- project.external_issue_tracker.close_issue(closed_via, issue)
+ project.external_issue_tracker.close_issue(closed_via, issue, current_user)
todo_service.close_issue(issue, current_user)
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 44de8eb6389..d2285a375a1 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -2,20 +2,26 @@
module Issues
class CreateService < Issues::BaseService
- include SpamCheckMethods
include ResolveDiscussions
def execute(skip_system_notes: false)
+ @request = params.delete(:request)
+ @spam_params = Spam::SpamActionService.filter_spam_params!(params)
+
@issue = BuildService.new(project, current_user, params).execute
- filter_spam_check_params
filter_resolve_discussion_params
create(@issue, skip_system_notes: skip_system_notes)
end
def before_create(issue)
- spam_check(issue, current_user, action: :create)
+ Spam::SpamActionService.new(
+ spammable: issue,
+ request: request,
+ user: current_user,
+ action: :create
+ ).execute(spam_params: spam_params)
# current_user (defined in BaseService) is not available within run_after_commit block
user = current_user
@@ -46,8 +52,10 @@ module Issues
private
+ attr_reader :request, :spam_params
+
def user_agent_detail_service
- UserAgentDetailService.new(@issue, @request)
+ UserAgentDetailService.new(@issue, request)
end
# Applies label "incident" (creates it if missing) to incident issues.
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 127ed04cf51..2906bdf62a7 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -2,12 +2,14 @@
module Issues
class UpdateService < Issues::BaseService
- include SpamCheckMethods
extend ::Gitlab::Utils::Override
def execute(issue)
handle_move_between_ids(issue)
- filter_spam_check_params
+
+ @request = params.delete(:request)
+ @spam_params = Spam::SpamActionService.filter_spam_params!(params)
+
change_issue_duplicate(issue)
move_issue_to_new_project(issue) || clone_issue(issue) || update_task_event(issue) || update(issue)
end
@@ -30,7 +32,14 @@ module Issues
end
def before_update(issue, skip_spam_check: false)
- spam_check(issue, current_user, action: :update) unless skip_spam_check
+ return if skip_spam_check
+
+ Spam::SpamActionService.new(
+ spammable: issue,
+ request: request,
+ user: current_user,
+ action: :update
+ ).execute(spam_params: spam_params)
end
def after_update(issue)
@@ -126,6 +135,8 @@ module Issues
private
+ attr_reader :request, :spam_params
+
def clone_issue(issue)
target_project = params.delete(:target_clone_project)
with_notes = params.delete(:clone_with_notes)
diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb
index 098aae9284c..bae8298d5c8 100644
--- a/app/services/jira/requests/base.rb
+++ b/app/services/jira/requests/base.rb
@@ -18,15 +18,15 @@ module Jira
request
end
+ private
+
+ attr_reader :jira_service, :project
+
# We have to add the context_path here because the Jira client is not taking it into account
def base_api_url
"#{context_path}/rest/api/#{api_version}"
end
- private
-
- attr_reader :jira_service, :project
-
def context_path
client.options[:context_path].to_s
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index b5c27caafa2..5c6e51201c2 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -16,7 +16,11 @@ module Members
enqueue_delete_todos(member) if downgrading_to_guest?
end
- member
+ if member.errors.any?
+ error(member.errors.full_messages.to_sentence, pass_back: { member: member })
+ else
+ success(member: member)
+ end
end
private
diff --git a/app/services/merge_requests/add_context_service.rb b/app/services/merge_requests/add_context_service.rb
index bb82fa23468..b693f8509a2 100644
--- a/app/services/merge_requests/add_context_service.rb
+++ b/app/services/merge_requests/add_context_service.rb
@@ -66,7 +66,8 @@ module MergeRequests
relative_order: index,
sha: sha,
authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
- committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
+ committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date]),
+ trailers: commit_hash.fetch(:trailers, {}).to_json
)
end
end
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 150ec85fca9..59d8f553eff 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -14,6 +14,7 @@ module MergeRequests
create_approval_note(merge_request)
mark_pending_todos_as_done(merge_request)
execute_approval_hooks(merge_request, current_user)
+ merge_request_activity_counter.track_approve_mr_action(user: current_user)
success
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 0613c061f2e..6bd31e26748 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -108,7 +108,7 @@ module MergeRequests
def filter_reviewer(merge_request)
return if params[:reviewer_ids].blank?
- unless can_admin_issuable?(merge_request) && merge_request.allows_reviewers?
+ unless can_admin_issuable?(merge_request)
params.delete(:reviewer_ids)
return
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 80991657688..12c901aa1a1 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -16,30 +16,17 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
- # Source project sets the default source branch removal setting
- merge_request.merge_params['force_remove_source_branch'] =
- if params.key?(:force_remove_source_branch)
- params.delete(:force_remove_source_branch)
- else
- merge_request.source_project.remove_source_branch_after_merge?
- end
+ # Force remove the source branch?
+ merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
+ # Only assign merge requests params that are allowed
self.params = assign_allowed_merge_params(merge_request, params)
+ # Filter out params that are either not allowed or invalid
filter_params(merge_request)
- # merge_request.assign_attributes(...) below is a Rails
- # method that only work if all the params it is passed have
- # corresponding fields in the database. As there are no fields
- # in the database for :add_label_ids and :remove_label_ids, we
- # need to remove them from the params before the call to
- # merge_request.assign_attributes(...)
- #
- # IssuableBaseService#process_label_ids takes care
- # of the removal.
- params[:label_ids] = process_label_ids(params, extra_label_ids: merge_request.label_ids.to_a)
-
- merge_request.assign_attributes(params.to_h.compact)
+ # Filter out :add_label_ids and :remove_label_ids params
+ filter_label_id_params
merge_request.compare_commits = []
set_merge_request_target_branch
@@ -74,6 +61,29 @@ module MergeRequests
:errors,
to: :merge_request
+ def force_remove_source_branch
+ if params.key?(:force_remove_source_branch)
+ params.delete(:force_remove_source_branch)
+ else
+ merge_request.source_project.remove_source_branch_after_merge?
+ end
+ end
+
+ def filter_label_id_params
+ # merge_request.assign_attributes(...) below is a Rails
+ # method that only work if all the params it is passed have
+ # corresponding fields in the database. As there are no fields
+ # in the database for :add_label_ids and :remove_label_ids, we
+ # need to remove them from the params before the call to
+ # merge_request.assign_attributes(...)
+ #
+ # IssuableBaseService#process_label_ids takes care
+ # of the removal.
+ params[:label_ids] = process_label_ids(params, extra_label_ids: merge_request.label_ids.to_a)
+
+ merge_request.assign_attributes(params.to_h.compact)
+ end
+
def find_source_project
source_project = project_from_params(:source_project)
return source_project if source_project.present? && can?(current_user, :create_merge_request_from, source_project)
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index 78b462174c9..b43e697d3ab 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -25,6 +25,7 @@ module MergeRequests
new_merge_request = create(merge_request)
if new_merge_request.valid?
+ merge_request_activity_counter.track_mr_create_from_issue(user: current_user)
SystemNoteService.new_merge_request(issue, project, current_user, new_merge_request)
success(new_merge_request)
diff --git a/app/services/merge_requests/mark_reviewer_reviewed_service.rb b/app/services/merge_requests/mark_reviewer_reviewed_service.rb
new file mode 100644
index 00000000000..766a4ca0a49
--- /dev/null
+++ b/app/services/merge_requests/mark_reviewer_reviewed_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class MarkReviewerReviewedService < MergeRequests::BaseService
+ def execute(merge_request)
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ reviewer = merge_request.find_reviewer(current_user)
+
+ if reviewer
+ return error("Failed to update reviewer") unless reviewer.update(state: :reviewed)
+
+ success
+ else
+ error("Reviewer not found")
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index f4454db0af8..fc4405ef704 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -8,6 +8,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::MergeBaseService
+ GENERIC_ERROR_MESSAGE = 'An error occurred while merging'
+
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request, options = {})
@@ -79,7 +81,7 @@ module MergeRequests
if commit_id
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
else
- raise_error('Conflicts detected during merge')
+ raise_error(GENERIC_ERROR_MESSAGE)
end
merge_request.update!(merge_commit_sha: commit_id)
@@ -96,7 +98,7 @@ module MergeRequests
"Something went wrong during merge pre-receive hook. #{e.message}".strip
rescue => e
handle_merge_error(log_message: e.message)
- raise_error('Something went wrong during merge')
+ raise_error(GENERIC_ERROR_MESSAGE)
end
def after_merge
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
index 96a2322f6a0..9fecab85cc1 100644
--- a/app/services/merge_requests/mergeability_check_service.rb
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -114,6 +114,7 @@ module MergeRequests
merge_to_ref_success = merge_to_ref
+ reload_merge_head_diff
update_diff_discussion_positions! if merge_to_ref_success
if merge_to_ref_success && can_git_merge?
@@ -123,6 +124,10 @@ module MergeRequests
end
end
+ def reload_merge_head_diff
+ MergeRequests::ReloadMergeHeadDiffService.new(merge_request).execute
+ end
+
def update_diff_discussion_positions!
Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
end
@@ -153,6 +158,7 @@ module MergeRequests
def merge_to_ref
params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) }
result = MergeRequests::MergeToRefService.new(project, merge_request.author, params).execute(merge_request)
+
result[:status] == :success
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index f04ec3c3e80..aafba9bfcef 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -9,6 +9,8 @@ module MergeRequests
class PostMergeService < MergeRequests::BaseService
include RemovesRefs
+ MAX_RETARGET_MERGE_REQUESTS = 4
+
def execute(merge_request)
merge_request.mark_as_merged
close_issues(merge_request)
@@ -18,6 +20,7 @@ module MergeRequests
merge_request_activity_counter.track_merge_mr_action(user: current_user)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
+ retarget_chain_merge_requests(merge_request)
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
@@ -28,6 +31,34 @@ module MergeRequests
private
+ def retarget_chain_merge_requests(merge_request)
+ return unless Feature.enabled?(:retarget_merge_requests, merge_request.target_project)
+
+ # we can only retarget MRs that are targeting the same project
+ # and have a remove source branch set
+ return unless merge_request.for_same_project? && merge_request.remove_source_branch?
+
+ # find another merge requests that
+ # - as a target have a current source project and branch
+ other_merge_requests = merge_request.source_project
+ .merge_requests
+ .opened
+ .by_target_branch(merge_request.source_branch)
+ .preload_source_project
+ .at_most(MAX_RETARGET_MERGE_REQUESTS)
+
+ other_merge_requests.find_each do |other_merge_request|
+ # Update only MRs on projects that we have access to
+ next unless can?(current_user, :update_merge_request, other_merge_request.source_project)
+
+ ::MergeRequests::UpdateService
+ .new(other_merge_request.source_project, current_user,
+ target_branch: merge_request.target_branch,
+ target_branch_was_deleted: true)
+ .execute(other_merge_request)
+ end
+ end
+
def close_issues(merge_request)
return unless merge_request.target_branch == project.default_branch
diff --git a/app/services/merge_requests/reload_merge_head_diff_service.rb b/app/services/merge_requests/reload_merge_head_diff_service.rb
new file mode 100644
index 00000000000..f02a9bd3139
--- /dev/null
+++ b/app/services/merge_requests/reload_merge_head_diff_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class ReloadMergeHeadDiffService
+ include BaseServiceUtility
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def execute
+ return error("default_merge_ref_for_diffs feature flag is disabled") unless enabled?
+ return error("Merge request has no merge ref head.") unless merge_request.merge_ref_head.present?
+
+ error_msg = recreate_merge_head_diff
+
+ return error(error_msg) if error_msg
+
+ success
+ end
+
+ private
+
+ attr_reader :merge_request
+
+ def enabled?
+ Feature.enabled?(:default_merge_ref_for_diffs, merge_request.project, default_enabled: :yaml)
+ end
+
+ def recreate_merge_head_diff
+ merge_request.merge_head_diff&.destroy!
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ merge_request.create_merge_head_diff!
+ end
+
+ # Reset the merge request so it won't load the merge head diff as the
+ # MergeRequest#merge_request_diff.
+ merge_request.reset
+
+ nil
+ rescue StandardError => e
+ message = "Failed to recreate merge head diff: #{e.message}"
+
+ Gitlab::AppLogger.error(message: message, merge_request_id: merge_request.id)
+ message
+ end
+ end
+end
diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb
index 3164d0b4069..f2bf5de61c1 100644
--- a/app/services/merge_requests/remove_approval_service.rb
+++ b/app/services/merge_requests/remove_approval_service.rb
@@ -16,6 +16,7 @@ module MergeRequests
reset_approvals_cache(merge_request)
create_note(merge_request)
+ merge_request_activity_counter.track_unapprove_mr_action(user: current_user)
end
success
diff --git a/app/services/merge_requests/request_review_service.rb b/app/services/merge_requests/request_review_service.rb
new file mode 100644
index 00000000000..b061ed45fee
--- /dev/null
+++ b/app/services/merge_requests/request_review_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class RequestReviewService < MergeRequests::BaseService
+ def execute(merge_request, user)
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ reviewer = merge_request.find_reviewer(user)
+
+ if reviewer
+ return error("Failed to update reviewer") unless reviewer.update(state: :unreviewed)
+
+ notify_reviewer(merge_request, user)
+
+ success
+ else
+ error("Reviewer not found")
+ end
+ end
+
+ private
+
+ def notify_reviewer(merge_request, reviewer)
+ notification_service.async.review_requested_of_merge_request(merge_request, current_user, reviewer)
+ todo_service.create_request_review_todo(merge_request, current_user, reviewer)
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index d2e5a2a1619..1707daff734 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -4,6 +4,12 @@ module MergeRequests
class UpdateService < MergeRequests::BaseService
extend ::Gitlab::Utils::Override
+ def initialize(project, user = nil, params = {})
+ super
+
+ @target_branch_was_deleted = @params.delete(:target_branch_was_deleted)
+ end
+
def execute(merge_request)
# We don't allow change of source/target projects and source branch
# after merge request was created
@@ -36,7 +42,9 @@ module MergeRequests
end
if merge_request.previous_changes.include?('target_branch')
- create_branch_change_note(merge_request, 'target',
+ create_branch_change_note(merge_request,
+ 'target',
+ target_branch_was_deleted ? 'delete' : 'update',
merge_request.previous_changes['target_branch'].first,
merge_request.target_branch)
@@ -87,12 +95,51 @@ module MergeRequests
MergeRequests::CloseService
end
+ def before_update(issuable, skip_spam_check: false)
+ return unless issuable.changed?
+
+ @issuable_changes = issuable.changes
+ end
+
def after_update(issuable)
issuable.cache_merge_request_closes_issues!(current_user)
+
+ return unless @issuable_changes
+
+ %w(title description).each do |action|
+ next unless @issuable_changes.key?(action)
+
+ # Track edits to title or description
+ #
+ merge_request_activity_counter
+ .public_send("track_#{action}_edit_action".to_sym, user: current_user) # rubocop:disable GitlabSecurity/PublicSend
+
+ # Track changes to Draft/WIP status
+ #
+ if action == "title"
+ old_title, new_title = @issuable_changes["title"]
+ old_title_wip = MergeRequest.work_in_progress?(old_title)
+ new_title_wip = MergeRequest.work_in_progress?(new_title)
+
+ if !old_title_wip && new_title_wip
+ # Marked as Draft/WIP
+ #
+ merge_request_activity_counter
+ .track_marked_as_draft_action(user: current_user)
+ elsif old_title_wip && !new_title_wip
+ # Unmarked as Draft/WIP
+ #
+ merge_request_activity_counter
+ .track_unmarked_as_draft_action(user: current_user)
+ end
+ end
+ end
end
private
+ attr_reader :target_branch_was_deleted
+
def handle_milestone_change(merge_request)
return if skip_milestone_email
@@ -109,6 +156,9 @@ module MergeRequests
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_assignable(merge_request, current_user, old_assignees)
+
+ new_assignees = merge_request.assignees - old_assignees
+ merge_request_activity_counter.track_users_assigned_to_mr(users: new_assignees)
end
def handle_reviewers_change(merge_request, old_reviewers)
@@ -117,11 +167,14 @@ module MergeRequests
notification_service.async.changed_reviewer_of_merge_request(merge_request, current_user, old_reviewers)
todo_service.reassigned_reviewable(merge_request, current_user, old_reviewers)
invalidate_cache_counts(merge_request, users: affected_reviewers.compact)
+
+ new_reviewers = merge_request.reviewers - old_reviewers
+ merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
end
- def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
+ def create_branch_change_note(issuable, branch_type, event_type, old_branch, new_branch)
SystemNoteService.change_branch(
- issuable, issuable.project, current_user, branch_type,
+ issuable, issuable.project, current_user, branch_type, event_type,
old_branch, new_branch)
end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
new file mode 100644
index 00000000000..45b4619ddbe
--- /dev/null
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class InProductMarketingEmailsService
+ include Gitlab::Experimentation::GroupTypes
+
+ INTERVAL_DAYS = [1, 5, 10].freeze
+ TRACKS = {
+ create: :git_write,
+ verify: :pipeline_created,
+ trial: :trial_started,
+ team: :user_added
+ }.freeze
+
+ def self.send_for_all_tracks_and_intervals
+ TRACKS.each_key do |track|
+ INTERVAL_DAYS.each do |interval|
+ new(track, interval).execute
+ end
+ end
+ end
+
+ def initialize(track, interval)
+ @track = track
+ @interval = interval
+ @sent_email_user_ids = []
+ end
+
+ def execute
+ groups_for_track.each_batch do |groups|
+ groups.each do |group|
+ send_email_for_group(group)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :track, :interval, :sent_email_user_ids
+
+ def send_email_for_group(group)
+ experiment_enabled_for_group = experiment_enabled_for_group?(group)
+ experiment_add_group(group, experiment_enabled_for_group)
+ return unless experiment_enabled_for_group
+
+ users_for_group(group).each do |user|
+ send_email(user, group) if can_perform_action?(user, group)
+ end
+ end
+
+ def experiment_enabled_for_group?(group)
+ Gitlab::Experimentation.in_experiment_group?(:in_product_marketing_emails, subject: group)
+ end
+
+ def experiment_add_group(group, experiment_enabled_for_group)
+ variant = experiment_enabled_for_group ? GROUP_EXPERIMENTAL : GROUP_CONTROL
+ Experiment.add_group(:in_product_marketing_emails, variant: variant, group: group)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def groups_for_track
+ onboarding_progress_scope = OnboardingProgress
+ .completed_actions_with_latest_in_range(completed_actions, range)
+ .incomplete_actions(incomplete_action)
+
+ Group.joins(:onboarding_progress).merge(onboarding_progress_scope)
+ end
+
+ def users_for_group(group)
+ group.users.where(email_opted_in: true)
+ .where.not(id: sent_email_user_ids)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def can_perform_action?(user, group)
+ case track
+ when :create
+ user.can?(:create_projects, group)
+ when :verify
+ user.can?(:create_projects, group)
+ when :trial
+ user.can?(:start_trial, group)
+ when :team
+ user.can?(:admin_group_member, group)
+ else
+ raise NotImplementedError, "No ability defined for track #{track}"
+ end
+ end
+
+ def send_email(user, group)
+ NotificationService.new.in_product_marketing(user.id, group.id, track, series)
+ sent_email_user_ids << user.id
+ end
+
+ def completed_actions
+ index = TRACKS.keys.index(track)
+ index == 0 ? [:created] : TRACKS.values[0..index - 1]
+ end
+
+ def range
+ (interval + 1).days.ago.beginning_of_day..(interval + 1).days.ago.end_of_day
+ end
+
+ def incomplete_action
+ TRACKS[track]
+ end
+
+ def series
+ INTERVAL_DAYS.index(interval)
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 04b7fba207b..488c847dcbb 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -27,7 +27,11 @@ module Notes
end
note_saved = note.with_transaction_returning_status do
- !only_commands && note.save
+ break false if only_commands
+
+ note.save.tap do
+ update_discussions(note)
+ end
end
when_saved(note) if note_saved
@@ -54,12 +58,17 @@ module Notes
@quick_actions_service ||= QuickActionsService.new(project, current_user)
end
- def when_saved(note)
+ def update_discussions(note)
+ # Ensure that individual notes that are promoted into discussions are
+ # updated in a transaction with the note creation to avoid inconsistencies:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/301237
if note.part_of_discussion? && note.discussion.can_convert_to_discussion?
note.discussion.convert_to_discussion!.save
note.clear_memoization(:discussion)
end
+ end
+ def when_saved(note)
todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
diff --git a/app/services/notification_recipients/build_service.rb b/app/services/notification_recipients/build_service.rb
index 040ecc29d3a..52070abbad7 100644
--- a/app/services/notification_recipients/build_service.rb
+++ b/app/services/notification_recipients/build_service.rb
@@ -36,5 +36,9 @@ module NotificationRecipients
def self.build_new_review_recipients(*args)
::NotificationRecipients::Builder::NewReview.new(*args).notification_recipients
end
+
+ def self.build_requested_review_recipients(*args)
+ ::NotificationRecipients::Builder::RequestReview.new(*args).notification_recipients
+ end
end
end
diff --git a/app/services/notification_recipients/builder/request_review.rb b/app/services/notification_recipients/builder/request_review.rb
new file mode 100644
index 00000000000..911d89c6a8e
--- /dev/null
+++ b/app/services/notification_recipients/builder/request_review.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module NotificationRecipients
+ module Builder
+ class RequestReview < Base
+ attr_reader :merge_request, :current_user, :reviewer
+
+ def initialize(merge_request, current_user, reviewer)
+ @merge_request, @current_user, @reviewer = merge_request, current_user, reviewer
+ end
+
+ def target
+ merge_request
+ end
+
+ def build!
+ add_recipients(reviewer, :mention, NotificationReason::REVIEW_REQUESTED)
+ end
+ end
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 5a71e0eac7c..50247532f69 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -265,6 +265,14 @@ class NotificationService
end
end
+ def review_requested_of_merge_request(merge_request, current_user, reviewer)
+ recipients = NotificationRecipients::BuildService.build_requested_review_recipients(merge_request, current_user, reviewer)
+
+ recipients.each do |recipient|
+ mailer.request_review_merge_request_email(recipient.user.id, merge_request.id, current_user.id, recipient.reason).deliver_later
+ end
+ end
+
# When we add labels to a merge request we should send an email to:
#
# * watchers of the mr's labels
@@ -664,6 +672,10 @@ class NotificationService
end
end
+ def in_product_marketing(user_id, group_id, track, series)
+ mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later
+ end
+
protected
def new_resource_email(target, method)
diff --git a/app/services/packages/create_package_service.rb b/app/services/packages/create_package_service.rb
index fcf252cf971..3dc06497d9f 100644
--- a/app/services/packages/create_package_service.rb
+++ b/app/services/packages/create_package_service.rb
@@ -9,7 +9,9 @@ module Packages
.packages
.with_package_type(package_type)
.safe_find_or_create_by!(name: name, version: version) do |package|
+ package.status = params[:status] if params[:status]
package.creator = package_creator
+
add_build_info(package)
end
end
@@ -29,8 +31,9 @@ module Packages
{
creator: package_creator,
name: params[:name],
- version: params[:version]
- }.merge(attrs)
+ version: params[:version],
+ status: params[:status]
+ }.compact.merge(attrs)
end
def package_creator
diff --git a/app/services/packages/debian/create_distribution_service.rb b/app/services/packages/debian/create_distribution_service.rb
new file mode 100644
index 00000000000..c6df033e3c1
--- /dev/null
+++ b/app/services/packages/debian/create_distribution_service.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class CreateDistributionService
+ def initialize(container, user, params)
+ @container, @params = container, params
+ @params[:creator] = user
+
+ @components = params.delete(:components) || ['main']
+
+ @architectures = params.delete(:architectures) || ['amd64']
+ @architectures += ['all']
+
+ @distribution = nil
+ @errors = []
+ end
+
+ def execute
+ create_distribution
+ end
+
+ private
+
+ attr_reader :container, :params, :components, :architectures, :distribution, :errors
+
+ def append_errors(record, prefix = '')
+ return if record.valid?
+
+ prefix = "#{prefix} " unless prefix.empty?
+ @errors += record.errors.full_messages.map { |message| "#{prefix}#{message}" }
+ end
+
+ def create_distribution
+ @distribution = container.debian_distributions.new(params)
+
+ append_errors(distribution)
+ return error unless errors.empty?
+
+ distribution.transaction do
+ if distribution.save
+ create_components
+ create_architectures
+
+ success
+ end
+ end || error
+ end
+
+ def create_components
+ create_objects(distribution.components, components, error_label: 'Component')
+ end
+
+ def create_architectures
+ create_objects(distribution.architectures, architectures, error_label: 'Architecture')
+ end
+
+ def create_objects(objects, object_names_from_params, error_label: )
+ object_names_from_params.each do |name|
+ new_object = objects.create(name: name)
+ append_errors(new_object, error_label)
+ raise ActiveRecord::Rollback unless new_object.persisted?
+ end
+ end
+
+ def success
+ ServiceResponse.success(payload: { distribution: distribution }, http_status: :created)
+ end
+
+ def error
+ ServiceResponse.error(message: errors.to_sentence, payload: { distribution: distribution })
+ end
+ end
+ end
+end
diff --git a/app/services/packages/debian/destroy_distribution_service.rb b/app/services/packages/debian/destroy_distribution_service.rb
new file mode 100644
index 00000000000..bef1127fece
--- /dev/null
+++ b/app/services/packages/debian/destroy_distribution_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class DestroyDistributionService
+ def initialize(distribution)
+ @distribution = distribution
+ end
+
+ def execute
+ destroy_distribution
+ end
+
+ private
+
+ def destroy_distribution
+ if @distribution.destroy
+ success
+ else
+ error("Unable to destroy Debian #{@distribution.model_name.human.downcase}")
+ end
+ end
+
+ def success
+ ServiceResponse.success
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message, payload: { distribution: @distribution })
+ end
+ end
+ end
+end
diff --git a/app/services/packages/debian/update_distribution_service.rb b/app/services/packages/debian/update_distribution_service.rb
new file mode 100644
index 00000000000..5bb59b854e9
--- /dev/null
+++ b/app/services/packages/debian/update_distribution_service.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class UpdateDistributionService
+ def initialize(distribution, params)
+ @distribution, @params = distribution, params
+
+ @components = params.delete(:components)
+
+ @architectures = params.delete(:architectures)
+ @architectures += ['all'] unless @architectures.nil?
+
+ @errors = []
+ end
+
+ def execute
+ update_distribution
+ end
+
+ private
+
+ attr_reader :distribution, :params, :components, :architectures, :errors
+
+ def append_errors(record, prefix = '')
+ return if record.valid?
+
+ prefix = "#{prefix} " unless prefix.empty?
+ @errors += record.errors.full_messages.map { |message| "#{prefix}#{message}" }
+ end
+
+ def update_distribution
+ distribution.transaction do
+ if distribution.update(params)
+ update_components if components
+ update_architectures if architectures
+
+ success
+ else
+ append_errors(distribution)
+ error
+ end
+ end || error
+ end
+
+ def update_components
+ update_objects(distribution.components, components, error_label: 'Component')
+ end
+
+ def update_architectures
+ update_objects(distribution.architectures, architectures, error_label: 'Architecture')
+ end
+
+ def update_objects(objects, object_names_from_params, error_label: )
+ current_object_names = objects.map(&:name)
+ missing_object_names = object_names_from_params - current_object_names
+ extra_object_names = current_object_names - object_names_from_params
+
+ missing_object_names.each do |name|
+ new_object = objects.create(name: name)
+ append_errors(new_object, error_label)
+ raise ActiveRecord::Rollback unless new_object.persisted?
+ end
+
+ extra_object_names.each do |name|
+ object = objects.with_name(name).first
+ raise ActiveRecord::Rollback unless object.destroy
+ end
+ end
+
+ def success
+ ServiceResponse.success(payload: { distribution: distribution })
+ end
+
+ def error
+ ServiceResponse.error(message: errors.to_sentence, payload: { distribution: distribution })
+ end
+ end
+ end
+end
diff --git a/app/services/packages/generic/create_package_file_service.rb b/app/services/packages/generic/create_package_file_service.rb
index b14b1c193ec..1451a022a39 100644
--- a/app/services/packages/generic/create_package_file_service.rb
+++ b/app/services/packages/generic/create_package_file_service.rb
@@ -15,13 +15,16 @@ module Packages
package_params = {
name: params[:package_name],
version: params[:package_version],
- build: params[:build]
+ build: params[:build],
+ status: params[:status]
}
package = ::Packages::Generic::FindOrCreatePackageService
.new(project, current_user, package_params)
.execute
+ package.update_column(:status, params[:status]) if params[:status] && params[:status] != package.status
+
package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present?
package
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 8ee449cbfdc..4c916d264a7 100644
--- a/app/services/packages/maven/find_or_create_package_service.rb
+++ b/app/services/packages/maven/find_or_create_package_service.rb
@@ -11,12 +11,7 @@ module Packages
.execute
unless Namespace::PackageSetting.duplicates_allowed?(package)
- files = package&.package_files || []
- current_maven_files = files.map { |file| extname(file.file_name) }
-
- if current_maven_files.compact.include?(extname(params[:file_name]))
- return ServiceResponse.error(message: 'Duplicate package is not allowed')
- end
+ return ServiceResponse.error(message: 'Duplicate package is not allowed') if target_package_is_duplicate?(package)
end
unless package
@@ -47,6 +42,7 @@ module Packages
package_params = {
name: package_name,
path: params[:path],
+ status: params[:status],
version: version
}
@@ -67,6 +63,17 @@ module Packages
File.extname(filename)
end
+
+ def target_package_is_duplicate?(package)
+ # duplicate metadata files can be uploaded multiple times
+ return false if package.version.nil?
+
+ package
+ .package_files
+ .map { |file| extname(file.file_name) }
+ .compact
+ .include?(extname(params[:file_name]))
+ end
end
end
end
diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb
index fc5d01a93a1..3dc9254718e 100644
--- a/app/services/pages/delete_service.rb
+++ b/app/services/pages/delete_service.rb
@@ -3,7 +3,13 @@
module Pages
class DeleteService < BaseService
def execute
- PagesRemoveWorker.perform_async(project.id)
+ project.mark_pages_as_not_deployed # prevents domain from updating config when deleted
+ project.pages_domains.delete_all
+
+ DestroyPagesDeploymentsWorker.perform_async(project.id)
+
+ # TODO: remove this call https://gitlab.com/gitlab-org/gitlab/-/issues/320775
+ PagesRemoveWorker.perform_async(project.id) if Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
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
new file mode 100644
index 00000000000..9b36b3f11b4
--- /dev/null
+++ b/app/services/pages/migrate_from_legacy_storage_service.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Pages
+ class MigrateFromLegacyStorageService
+ def initialize(logger, migration_threads:, batch_size:, ignore_invalid_entries:)
+ @logger = logger
+ @migration_threads = migration_threads
+ @batch_size = batch_size
+ @ignore_invalid_entries = ignore_invalid_entries
+
+ @migrated = 0
+ @errored = 0
+ @counters_lock = Mutex.new
+ end
+
+ def execute
+ @queue = SizedQueue.new(1)
+
+ threads = start_migration_threads
+
+ ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: @batch_size) do |batch|
+ @queue.push(batch)
+ end
+
+ @queue.close
+
+ @logger.info("Waiting for threads to finish...")
+ threads.each(&:join)
+
+ { migrated: @migrated, errored: @errored }
+ end
+
+ def start_migration_threads
+ Array.new(@migration_threads) 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("#{@migrated} projects are migrated successfully, #{@errored} projects failed to be migrated")
+ rescue => 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("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).execute
+ end
+
+ if result[:status] == :success
+ @logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time.round(2)} seconds")
+ @counters_lock.synchronize { @migrated += 1 }
+ else
+ @logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time.round(2)} seconds: #{result[:message]}")
+ @counters_lock.synchronize { @errored += 1 }
+ end
+ rescue => e
+ @counters_lock.synchronize { @errored += 1 }
+ @logger.error("project_id: #{project&.id} #{project&.pages_path} failed to be migrated: #{e.message}")
+ 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
index dac994b2ccc..63410b9fe4a 100644
--- a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
+++ b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb
@@ -9,8 +9,9 @@ module Pages
attr_reader :project
- def initialize(project)
+ def initialize(project, ignore_invalid_entries: false)
@project = project
+ @ignore_invalid_entries = ignore_invalid_entries
end
def execute
@@ -26,7 +27,7 @@ module Pages
private
def execute_unsafe
- zip_result = ::Pages::ZipDirectoryService.new(project.pages_path).execute
+ zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute
if zip_result[:status] == :error
if !project.pages_metadatum&.reload&.pages_deployment &&
diff --git a/app/services/pages/zip_directory_service.rb b/app/services/pages/zip_directory_service.rb
index ba7a8571e88..ae08d40ee37 100644
--- a/app/services/pages/zip_directory_service.rb
+++ b/app/services/pages/zip_directory_service.rb
@@ -10,12 +10,17 @@ module Pages
PUBLIC_DIR = 'public'
- def initialize(input_dir)
+ 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 error("Can not find valid public dir in #{@input_dir}") unless valid_path?(public_dir)
+ unless resolve_public_dir
+ return error("Can not find valid public dir in #{@input_dir}")
+ end
output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects
@@ -35,24 +40,36 @@ module Pages
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
+ raise InvalidEntryError, "#{disk_file_path} is invalid, input_dir: #{@input_dir}"
end
- case File.lstat(disk_file_path).ftype
+ 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
+ raise InvalidEntryError, "#{disk_file_path} has invalid ftype: #{ftype}, input_dir: #{@input_dir}"
end
- rescue InvalidEntryError => e
+ 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)
@@ -70,31 +87,11 @@ module Pages
end
end
- # that should never happen, but we want to be safer
- # in theory without this we would allow to use symlinks
- # to pack any directory on disk
- # it isn't possible because SafeZip doesn't extract such archives
+ # 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 + "/")
- # happens if target of symlink isn't there
- rescue => e
- Gitlab::ErrorTracking.track_exception(e, input_dir: real_dir, disk_file_path: disk_file_path)
-
- false
- end
-
- def real_dir
- strong_memoize(:real_dir) do
- File.realpath(@input_dir) rescue nil
- end
- end
-
- def public_dir
- strong_memoize(:public_dir) do
- File.join(real_dir, PUBLIC_DIR) rescue nil
- end
end
end
end
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index 014fb0e3ed3..2ba64b73699 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -3,9 +3,8 @@
module Projects
module Alerting
class NotifyService
- include BaseServiceUtility
- include Gitlab::Utils::StrongMemoize
- include ::IncidentManagement::Settings
+ extend ::Gitlab::Utils::Override
+ include ::AlertManagement::AlertProcessing
def initialize(project, payload)
@project = project
@@ -22,8 +21,7 @@ module Projects
process_alert
return bad_request unless alert.persisted?
- process_incident_issues if process_issues?
- send_alert_email if send_email?
+ complete_post_processing_tasks
ServiceResponse.success
end
@@ -32,93 +30,15 @@ module Projects
attr_reader :project, :payload, :integration
- def process_alert
- if alert.persisted?
- process_existing_alert
- else
- create_alert
- end
- end
-
- def process_existing_alert
- if incoming_payload.ends_at.present?
- process_resolved_alert
- else
- alert.register_new_event!
- end
-
- alert
- end
-
- def process_resolved_alert
- return unless auto_close_incident?
-
- if alert.resolve(incoming_payload.ends_at)
- close_issue(alert.issue)
- end
-
- alert
- end
-
- def close_issue(issue)
- return if issue.blank? || issue.closed?
-
- ::Issues::CloseService
- .new(project, User.alert_bot)
- .execute(issue, system_note: false)
-
- SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if issue.reset.closed?
- end
-
- def create_alert
- return unless alert.save
-
- alert.execute_services
- SystemNoteService.create_new_alert(alert, notification_source)
- end
-
- def process_incident_issues
- return if alert.issue || alert.resolved?
-
- ::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
- end
-
- def send_alert_email
- notification_service
- .async
- .prometheus_alerts_fired(project, [alert])
- end
-
- def alert
- strong_memoize(:alert) do
- existing_alert || new_alert
- end
- end
-
- def existing_alert
- return unless incoming_payload.gitlab_fingerprint
-
- AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first
- end
-
- def new_alert
- AlertManagement::Alert.new(**incoming_payload.alert_params, ended_at: nil)
- end
-
- def incoming_payload
- strong_memoize(:incoming_payload) do
- Gitlab::AlertManagement::Payload.parse(project, payload.to_h)
- end
+ def valid_payload_size?
+ Gitlab::Utils::DeepSize.new(payload).valid?
end
- def notification_source
+ override :alert_source
+ def alert_source
alert.monitoring_tool || integration&.name || 'Generic Alert Endpoint'
end
- def valid_payload_size?
- Gitlab::Utils::DeepSize.new(payload).valid?
- end
-
def active_integration?
integration&.active?
end
diff --git a/app/services/projects/branches_by_mode_service.rb b/app/services/projects/branches_by_mode_service.rb
new file mode 100644
index 00000000000..fb66bfa073b
--- /dev/null
+++ b/app/services/projects/branches_by_mode_service.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# Projects::BranchesByModeService uses Gitaly page-token pagination
+# in order to optimally fetch branches.
+# The drawback of the page-token pagination is that it doesn't provide
+# an option of going to the previous page of the collection.
+# That's why we need to fall back to offset pagination when previous page
+# is requested.
+class Projects::BranchesByModeService
+ include Gitlab::Routing
+
+ attr_reader :project, :params
+
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ return fetch_branches_via_gitaly_pagination if use_gitaly_pagination?
+
+ fetch_branches_via_offset_pagination
+ end
+
+ private
+
+ def mode
+ params[:mode]
+ end
+
+ def by_mode(branches)
+ return branches unless %w[active stale].include?(mode)
+
+ branches.select { |b| b.state.to_s == mode }
+ end
+
+ def use_gitaly_pagination?
+ return false if params[:page].present? || params[:search].present?
+
+ Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true)
+ end
+
+ def fetch_branches_via_offset_pagination
+ branches = BranchesFinder.new(project.repository, params).execute
+ branches = Kaminari.paginate_array(by_mode(branches)).page(params[:page])
+
+ branches_with_links(branches, last_page: branches.last_page?)
+ end
+
+ def fetch_branches_via_gitaly_pagination
+ per_page = Kaminari.config.default_per_page
+ options = params.merge(per_page: per_page + 1, page_token: params[:page_token])
+
+ branches = BranchesFinder.new(project.repository, options).execute(gitaly_pagination: true)
+
+ # Branch is stale if it hasn't been updated for 3 months
+ # This logic is specified in Gitlab Rails and isn't specified in Gitaly
+ # To display stale branches we fetch branches sorted as most-stale-at-the-top
+ # If the result contains active branches we filter them out and define that no more stale branches left
+ # Same logic applies to fetching active branches
+ branches = by_mode(branches)
+ last_page = branches.size <= per_page
+
+ branches = branches.take(per_page) # rubocop:disable CodeReuse/ActiveRecord
+
+ branches_with_links(branches, last_page: last_page)
+ end
+
+ def branches_with_links(branches, last_page:)
+ # To fall back to offset pagination we need to track current page via offset param
+ # And increase it whenever we go to the next page
+ previous_offset = params[:offset].to_i
+
+ previous_path, next_path = nil, nil
+
+ return [branches, previous_path, next_path] if branches.blank?
+
+ unless last_page
+ next_path = project_branches_filtered_path(project, state: mode, page_token: branches.last.name, sort: params[:sort], offset: previous_offset + 1)
+ end
+
+ if previous_offset > 0
+ previous_path = project_branches_filtered_path(project, state: mode, sort: params[:sort], page: previous_offset, offset: previous_offset - 1)
+ end
+
+ [branches, previous_path, next_path]
+ end
+end
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
index 6e3b320afbe..7bcaee75813 100644
--- a/app/services/projects/cleanup_service.rb
+++ b/app/services/projects/cleanup_service.rb
@@ -40,7 +40,7 @@ module Projects
apply_bfg_object_map!
# Remove older objects that are no longer referenced
- GitGarbageCollectWorker.new.perform(project.id, :prune, "project_cleanup:gc:#{project.id}")
+ Projects::GitGarbageCollectWorker.new.perform(project.id, :prune, "project_cleanup:gc:#{project.id}")
# The cache may now be inaccurate, and holding onto it could prevent
# bugs assuming the presence of some object from manifesting for some
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index af0107436c8..793d2fec033 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -25,6 +25,7 @@ module Projects
result[:before_truncate_size] = before_truncate_size
result[:after_truncate_size] = after_truncate_size
result[:before_delete_size] = tags.size
+ result[:deleted_size] = result[:deleted]&.size
result[:status] = :error if before_truncate_size != after_truncate_size
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index a01db4b498c..08f569662a8 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -127,7 +127,7 @@ module Projects
access_level: group_access_level)
end
- if Feature.enabled?(:specialized_project_authorization_workers)
+ if Feature.enabled?(:specialized_project_authorization_workers, default_enabled: :yaml)
AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
# AuthorizedProjectsWorker uses an exclusive lease per user but
# specialized workers might have synchronization issues. Until we
@@ -210,16 +210,22 @@ module Projects
end
def set_project_name_from_path
- # Set project name from path
- if @project.name.present? && @project.path.present?
- # if both name and path set - everything is ok
- elsif @project.path.present?
+ # if both name and path set - everything is ok
+ return if @project.name.present? && @project.path.present?
+
+ if @project.path.present?
# Set project name from path
@project.name = @project.path.dup
elsif @project.name.present?
# For compatibility - set path from name
- # TODO: remove this in 8.0
- @project.path = @project.name.dup.parameterize
+ @project.path = @project.name.dup
+
+ # TODO: Retained for backwards compatibility. Remove in API v5.
+ # When removed, validation errors will get bubbled up automatically.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52725
+ unless @project.path.match?(Gitlab::PathRegex.project_path_format_regex)
+ @project.path = @project.path.parameterize
+ end
end
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 050bfdd862d..fd9b64a4ee0 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -43,8 +43,8 @@ module Projects
def new_fork_params
new_params = {
forked_from_project: @project,
- visibility_level: allowed_visibility_level,
- description: @project.description,
+ visibility_level: target_visibility_level,
+ description: target_description,
name: target_name,
path: target_path,
shared_runners_enabled: @project.shared_runners_enabled,
@@ -107,6 +107,10 @@ module Projects
@target_name ||= @params[:name] || @project.name
end
+ def target_description
+ @target_description ||= @params[:description] || @project.description
+ end
+
def target_namespace
@target_namespace ||= @params[:namespace] || current_user.namespace
end
@@ -115,8 +119,9 @@ module Projects
@skip_disk_validation ||= @params[:skip_disk_validation] || false
end
- def allowed_visibility_level
+ def target_visibility_level
target_level = [@project.visibility_level, target_namespace.visibility_level].min
+ target_level = [target_level, Gitlab::VisibilityLevel.level_value(params[:visibility])].min if params.key?(:visibility)
Gitlab::VisibilityLevel.closest_allowed_level(target_level)
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 031b99753c3..c2a8db7b657 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -86,11 +86,11 @@ module Projects
end
def repo_saver
- Gitlab::ImportExport::RepoSaver.new(project: project, shared: shared)
+ Gitlab::ImportExport::RepoSaver.new(exportable: project, shared: shared)
end
def wiki_repo_saver
- Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: shared)
+ Gitlab::ImportExport::WikiRepoSaver.new(exportable: project, shared: shared)
end
def lfs_saver
@@ -102,7 +102,7 @@ module Projects
end
def design_repo_saver
- Gitlab::ImportExport::DesignRepoSaver.new(project: project, shared: shared)
+ Gitlab::ImportExport::DesignRepoSaver.new(exportable: project, shared: shared)
end
def cleanup
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 25d46ada885..29e92d725e2 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -80,6 +80,10 @@ module Projects
end
def deploy_to_legacy_storage(artifacts_path)
+ # path today used by one project can later be used by another
+ # so we can't really scope this feature flag by project or group
+ return unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
+
# Create temporary directory in which we will extract the artifacts
make_secure_tmp_dir(tmp_path) do |tmp_path|
extract_archive!(artifacts_path, tmp_path)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 50a544ed1a5..8384bfa813f 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -56,11 +56,25 @@ module Projects
raise ValidationError.new(s_('UpdateProject|Cannot rename project because it contains container registry tags!'))
end
- if changing_default_branch?
- raise ValidationError.new(s_("UpdateProject|Could not set the default branch")) unless project.change_head(params[:default_branch])
+ validate_default_branch_change
+ end
+
+ def validate_default_branch_change
+ return unless changing_default_branch?
+
+ previous_default_branch = project.default_branch
+
+ if project.change_head(params[:default_branch])
+ after_default_branch_change(previous_default_branch)
+ else
+ raise ValidationError.new(s_("UpdateProject|Could not set the default branch"))
end
end
+ def after_default_branch_change(previous_default_branch)
+ # overridden by EE module
+ end
+
def remove_unallowed_params
params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, project)
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index de1cd7cd981..ea90d8e3dd8 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -164,6 +164,7 @@ module QuickActions
next unless definition
definition.execute(self, arg)
+ usage_ping_tracking(name, arg)
end
end
@@ -178,6 +179,14 @@ module QuickActions
ext.references(type)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def usage_ping_tracking(quick_action_name, arg)
+ Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter.track_unique_action(
+ quick_action_name,
+ args: arg&.strip,
+ user: current_user
+ )
+ end
end
end
diff --git a/app/services/repositories/changelog_service.rb b/app/services/repositories/changelog_service.rb
new file mode 100644
index 00000000000..96a63865a49
--- /dev/null
+++ b/app/services/repositories/changelog_service.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+module Repositories
+ # A service class for generating a changelog section.
+ class ChangelogService
+ DEFAULT_TRAILER = 'Changelog'
+ DEFAULT_FILE = 'CHANGELOG.md'
+
+ # The `project` specifies the `Project` to generate the changelog section
+ # for.
+ #
+ # The `user` argument specifies a `User` to use for committing the changes
+ # to the Git repository.
+ #
+ # The `version` arguments must be a version `String` using semantic
+ # versioning as the format.
+ #
+ # The arguments `from` and `to` must specify a Git ref or SHA to use for
+ # fetching the commits to include in the changelog. The SHA/ref set in the
+ # `from` argument isn't included in the list.
+ #
+ # The `date` argument specifies the date of the release, and defaults to the
+ # current time/date.
+ #
+ # The `branch` argument specifies the branch to commit the changes to. The
+ # branch must already exist.
+ #
+ # The `trailer` argument is the Git trailer to use for determining what
+ # commits to include in the changelog.
+ #
+ # The `file` arguments specifies the name/path of the file to commit the
+ # changes to. If the file doesn't exist, it's created automatically.
+ #
+ # The `message` argument specifies the commit message to use when committing
+ # the changelog changes.
+ #
+ # rubocop: disable Metrics/ParameterLists
+ def initialize(
+ project,
+ user,
+ version:,
+ to:,
+ from: nil,
+ date: DateTime.now,
+ branch: project.default_branch_or_master,
+ trailer: DEFAULT_TRAILER,
+ file: DEFAULT_FILE,
+ message: "Add changelog for version #{version}"
+ )
+ @project = project
+ @user = user
+ @version = version
+ @from = from
+ @to = to
+ @date = date
+ @branch = branch
+ @trailer = trailer
+ @file = file
+ @message = message
+ end
+ # rubocop: enable Metrics/ParameterLists
+
+ def execute
+ from = start_of_commit_range
+
+ # For every entry we want to only include the merge request that
+ # originally introduced the commit, which is the oldest merge request that
+ # contains the commit. We fetch there merge requests in batches, reducing
+ # the number of SQL queries needed to get this data.
+ mrs_finder = MergeRequests::OldestPerCommitFinder.new(@project)
+ config = Gitlab::Changelog::Config.from_git(@project)
+ release = Gitlab::Changelog::Release
+ .new(version: @version, date: @date, config: config)
+
+ commits =
+ CommitsWithTrailerFinder.new(project: @project, from: from, to: @to)
+
+ commits.each_page(@trailer) do |page|
+ mrs = mrs_finder.execute(page)
+
+ # Preload the authors. This ensures we only need a single SQL query per
+ # batch of commits, instead of needing a query for every commit.
+ page.each(&:lazy_author)
+
+ page.each do |commit|
+ release.add_entry(
+ title: commit.title,
+ commit: commit,
+ category: commit.trailers.fetch(@trailer),
+ author: commit.author,
+ merge_request: mrs[commit.id]
+ )
+ end
+ end
+
+ Gitlab::Changelog::Committer
+ .new(@project, @user)
+ .commit(release: release, file: @file, branch: @branch, message: @message)
+ end
+
+ def start_of_commit_range
+ return @from if @from
+
+ if (prev_tag = PreviousTagFinder.new(@project).execute(@version))
+ return prev_tag.target_commit.id
+ end
+
+ raise(
+ Gitlab::Changelog::Error,
+ 'The commit start range is unspecified, and no previous tag ' \
+ 'could be found to use instead'
+ )
+ end
+ end
+end
diff --git a/app/services/repositories/housekeeping_service.rb b/app/services/repositories/housekeeping_service.rb
index 6a2fa95d25f..de80390e60b 100644
--- a/app/services/repositories/housekeeping_service.rb
+++ b/app/services/repositories/housekeeping_service.rb
@@ -45,7 +45,7 @@ module Repositories
private
def execute_gitlab_shell_gc(lease_uuid)
- GitGarbageCollectWorker.perform_async(@resource.id, task, lease_key, lease_uuid)
+ @resource.git_garbage_collect_worker_klass.perform_async(@resource.id, task, lease_key, lease_uuid)
ensure
if pushes_since_gc >= gc_period
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 70e09be9407..36858f33b49 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -10,7 +10,7 @@ module ResourceAccessTokens
end
def execute
- return error("User does not have permission to create #{resource_type} Access Token") unless has_permission_to_create?
+ return error("User does not have permission to create #{resource_type} access token") unless has_permission_to_create?
user = create_user
@@ -26,6 +26,7 @@ module ResourceAccessTokens
token_response = create_personal_access_token(user)
if token_response.success?
+ log_event(token_response.payload[:personal_access_token])
success(token_response.payload[:personal_access_token])
else
delete_failed_user(user)
@@ -105,6 +106,10 @@ module ResourceAccessTokens
resource.add_user(user, :maintainer, expires_at: params[:expires_at])
end
+ def log_event(token)
+ ::Gitlab::AppLogger.info "PROJECT ACCESS TOKEN CREATION: created_by: #{current_user.username}, project_id: #{resource.id}, token_user: #{token.user.name}, token_id: #{token.id}"
+ end
+
def error(message)
ServiceResponse.error(message: message)
end
@@ -114,3 +119,5 @@ module ResourceAccessTokens
end
end
end
+
+ResourceAccessTokens::CreateService.prepend_if_ee('EE::ResourceAccessTokens::CreateService')
diff --git a/app/services/resource_access_tokens/revoke_service.rb b/app/services/resource_access_tokens/revoke_service.rb
index ece928dac31..59402701ddc 100644
--- a/app/services/resource_access_tokens/revoke_service.rb
+++ b/app/services/resource_access_tokens/revoke_service.rb
@@ -21,6 +21,8 @@ module ResourceAccessTokens
destroy_bot_user
+ log_event
+
success("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.")
rescue StandardError => error
log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
@@ -57,6 +59,10 @@ module ResourceAccessTokens
end
end
+ def log_event
+ ::Gitlab::AppLogger.info "PROJECT ACCESS TOKEN REVOCATION: revoked_by: #{current_user.username}, project_id: #{resource.id}, token_user: #{access_token.user.name}, token_id: #{access_token.id}"
+ end
+
def error(message)
ServiceResponse.error(message: message)
end
@@ -66,3 +72,5 @@ module ResourceAccessTokens
end
end
end
+
+ResourceAccessTokens::RevokeService.prepend_if_ee('EE::ResourceAccessTokens::RevokeService')
diff --git a/app/services/resource_events/base_change_timebox_service.rb b/app/services/resource_events/base_change_timebox_service.rb
index 5c83f7b12f7..d802bbee107 100644
--- a/app/services/resource_events/base_change_timebox_service.rb
+++ b/app/services/resource_events/base_change_timebox_service.rb
@@ -2,12 +2,11 @@
module ResourceEvents
class BaseChangeTimeboxService
- attr_reader :resource, :user, :event_created_at
+ attr_reader :resource, :user
- def initialize(resource, user, created_at: Time.current)
+ def initialize(resource, user)
@resource = resource
@user = user
- @event_created_at = created_at
end
def execute
@@ -27,7 +26,7 @@ module ResourceEvents
{
user_id: user.id,
- created_at: event_created_at,
+ created_at: resource.system_note_timestamp,
key => resource.id
}
end
diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb
index dcdf87599ac..24935a3327a 100644
--- a/app/services/resource_events/change_milestone_service.rb
+++ b/app/services/resource_events/change_milestone_service.rb
@@ -4,8 +4,8 @@ module ResourceEvents
class ChangeMilestoneService < BaseChangeTimeboxService
attr_reader :milestone, :old_milestone
- def initialize(resource, user, created_at: Time.current, old_milestone:)
- super(resource, user, created_at: created_at)
+ def initialize(resource, user, old_milestone:)
+ super(resource, user)
@milestone = resource&.milestone
@old_milestone = old_milestone
diff --git a/app/services/security/ci_configuration/sast_create_service.rb b/app/services/security/ci_configuration/sast_create_service.rb
new file mode 100644
index 00000000000..8fc3b8d078c
--- /dev/null
+++ b/app/services/security/ci_configuration/sast_create_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Security
+ module CiConfiguration
+ class SastCreateService < ::BaseService
+ def initialize(project, current_user, params)
+ @project = project
+ @current_user = current_user
+ @params = params
+ @branch_name = @project.repository.next_branch('set-sast-config')
+ end
+
+ def execute
+ attributes_for_commit = attributes
+ result = ::Files::MultiService.new(@project, @current_user, attributes_for_commit).execute
+
+ if result[:status] == :success
+ result[:success_path] = successful_change_path
+ track_event(attributes_for_commit)
+ else
+ result[:errors] = result[:message]
+ end
+
+ result
+
+ rescue Gitlab::Git::PreReceiveError => e
+ { status: :error, errors: e.message }
+ end
+
+ private
+
+ def attributes
+ actions = Security::CiConfiguration::SastBuildActions.new(@project.auto_devops_enabled?, @params, existing_gitlab_ci_content).generate
+
+ @project.repository.add_branch(@current_user, @branch_name, @project.default_branch)
+ message = _('Set .gitlab-ci.yml to enable or configure SAST')
+
+ {
+ commit_message: message,
+ branch_name: @branch_name,
+ start_branch: @branch_name,
+ actions: actions
+ }
+ end
+
+ def existing_gitlab_ci_content
+ gitlab_ci_yml = @project.repository.gitlab_ci_yml_for(@project.repository.root_ref_sha)
+ YAML.safe_load(gitlab_ci_yml) if gitlab_ci_yml
+ end
+
+ def successful_change_path
+ description = _('Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings.')
+ merge_request_params = { source_branch: @branch_name, description: description }
+ Gitlab::Routing.url_helpers.project_new_merge_request_url(@project, merge_request: merge_request_params)
+ end
+
+ def track_event(attributes_for_commit)
+ action = attributes_for_commit[:actions].first
+
+ Gitlab::Tracking.event(
+ self.class.to_s, action[:action], label: action[:default_values_overwritten].to_s
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/security/ci_configuration/sast_parser_service.rb b/app/services/security/ci_configuration/sast_parser_service.rb
new file mode 100644
index 00000000000..a8fe5764d19
--- /dev/null
+++ b/app/services/security/ci_configuration/sast_parser_service.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module Security
+ module CiConfiguration
+ # This class parses SAST template file and .gitlab-ci.yml to populate default and current values into the JSON
+ # read from app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
+ class SastParserService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ SAST_UI_SCHEMA_PATH = 'app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json'
+
+ def initialize(project)
+ @project = project
+ end
+
+ def configuration
+ result = Gitlab::Json.parse(File.read(Rails.root.join(SAST_UI_SCHEMA_PATH))).with_indifferent_access
+ populate_default_value_for(result, :global)
+ populate_default_value_for(result, :pipeline)
+ fill_current_value_with_default_for(result, :global)
+ fill_current_value_with_default_for(result, :pipeline)
+ populate_current_value_for(result, :global)
+ populate_current_value_for(result, :pipeline)
+
+ fill_current_value_with_default_for_analyzers(result)
+ populate_current_value_for_analyzers(result)
+
+ result
+ end
+
+ private
+
+ def sast_template_content
+ Gitlab::Template::GitlabCiYmlTemplate.find('SAST').content
+ end
+
+ def populate_default_value_for(config, level)
+ set_each(config[level], key: :default_value, with: sast_template_attributes)
+ end
+
+ def populate_current_value_for(config, level)
+ set_each(config[level], key: :value, with: gitlab_ci_yml_attributes)
+ end
+
+ def fill_current_value_with_default_for(config, level)
+ set_each(config[level], key: :value, with: sast_template_attributes)
+ end
+
+ def set_each(config_attributes, key:, with:)
+ config_attributes.each do |entity|
+ entity[key] = with[entity[:field]] if with[entity[:field]]
+ end
+ end
+
+ def fill_current_value_with_default_for_analyzers(result)
+ result[:analyzers].each do |analyzer|
+ analyzer[:variables].each do |entity|
+ entity[:value] = entity[:default_value] if entity[:default_value]
+ end
+ end
+ end
+
+ def populate_current_value_for_analyzers(result)
+ result[:analyzers].each do |analyzer|
+ analyzer[:enabled] = analyzer_enabled?(analyzer[:name])
+ populate_current_value_for(analyzer, :variables)
+ end
+ end
+
+ def analyzer_enabled?(analyzer_name)
+ # Unless explicitly listed in the excluded analyzers, consider it enabled
+ sast_excluded_analyzers.exclude?(analyzer_name)
+ end
+
+ def sast_excluded_analyzers
+ strong_memoize(:sast_excluded_analyzers) do
+ all_analyzers = Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS.split(', ') rescue []
+ enabled_analyzers = sast_default_analyzers.split(',').map(&:strip) rescue []
+
+ excluded_analyzers = gitlab_ci_yml_attributes["SAST_EXCLUDED_ANALYZERS"] || sast_template_attributes["SAST_EXCLUDED_ANALYZERS"]
+ excluded_analyzers = excluded_analyzers.split(',').map(&:strip) rescue []
+ ((all_analyzers - enabled_analyzers) + excluded_analyzers).uniq
+ end
+ end
+
+ def sast_default_analyzers
+ @sast_default_analyzers ||= gitlab_ci_yml_attributes["SAST_DEFAULT_ANALYZERS"] || sast_template_attributes["SAST_DEFAULT_ANALYZERS"]
+ end
+
+ def sast_template_attributes
+ @sast_template_attributes ||= build_sast_attributes(sast_template_content)
+ end
+
+ def gitlab_ci_yml_attributes
+ @gitlab_ci_yml_attributes ||= begin
+ config_content = @project.repository.blob_data_at(@project.repository.root_ref_sha, ci_config_file)
+ return {} unless config_content
+
+ build_sast_attributes(config_content)
+ end
+ end
+
+ def ci_config_file
+ '.gitlab-ci.yml'
+ end
+
+ def build_sast_attributes(content)
+ options = { project: @project, user: current_user, sha: @project.repository.commit.sha }
+ yaml_result = Gitlab::Ci::YamlProcessor.new(content, options).execute
+ return {} unless yaml_result.valid?
+
+ sast_attributes = yaml_result.build_attributes(:sast)
+ extract_required_attributes(sast_attributes)
+ end
+
+ def extract_required_attributes(attributes)
+ result = {}
+ attributes[:yaml_variables].each do |variable|
+ result[variable[:key]] = variable[:value]
+ end
+
+ result[:stage] = attributes[:stage]
+ result.with_indifferent_access
+ end
+ end
+ end
+end
diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb
index 278857b7933..415cfcb7d8f 100644
--- a/app/services/snippets/base_service.rb
+++ b/app/services/snippets/base_service.rb
@@ -2,8 +2,6 @@
module Snippets
class BaseService < ::BaseService
- include SpamCheckMethods
-
UPDATE_COMMIT_MSG = 'Update snippet'
INITIAL_COMMIT_MSG = 'Initial commit'
@@ -18,8 +16,6 @@ module Snippets
input_actions = Array(@params.delete(:snippet_actions).presence)
@snippet_actions = SnippetInputActionCollection.new(input_actions, allowed_actions: restricted_files_actions)
-
- filter_spam_check_params
end
private
diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb
index 0881be73eaf..802bfd813dc 100644
--- a/app/services/snippets/create_service.rb
+++ b/app/services/snippets/create_service.rb
@@ -3,20 +3,32 @@
module Snippets
class CreateService < Snippets::BaseService
def execute
+ # NOTE: disable_spam_action_service can be removed when the ':snippet_spam' feature flag is removed.
+ disable_spam_action_service = params.delete(:disable_spam_action_service) == true
+ @request = params.delete(:request)
+ @spam_params = Spam::SpamActionService.filter_spam_params!(params)
+
@snippet = build_from_params
return invalid_params_error(@snippet) unless valid_params?
- unless visibility_allowed?(@snippet, @snippet.visibility_level)
- return forbidden_visibility_error(@snippet)
+ unless visibility_allowed?(snippet, snippet.visibility_level)
+ return forbidden_visibility_error(snippet)
end
@snippet.author = current_user
- spam_check(@snippet, current_user, action: :create)
+ unless disable_spam_action_service
+ Spam::SpamActionService.new(
+ spammable: @snippet,
+ request: request,
+ user: current_user,
+ action: :create
+ ).execute(spam_params: spam_params)
+ end
if save_and_commit
- UserAgentDetailService.new(@snippet, @request).create
+ UserAgentDetailService.new(@snippet, request).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create)
move_temporary_files
@@ -29,6 +41,8 @@ module Snippets
private
+ attr_reader :snippet, :request, :spam_params
+
def build_from_params
if project
project.snippets.build(create_params)
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index b982ff98747..5b427817a02 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -7,6 +7,11 @@ module Snippets
UpdateError = Class.new(StandardError)
def execute(snippet)
+ # NOTE: disable_spam_action_service can be removed when the ':snippet_spam' feature flag is removed.
+ disable_spam_action_service = params.delete(:disable_spam_action_service) == true
+ @request = params.delete(:request)
+ @spam_params = Spam::SpamActionService.filter_spam_params!(params)
+
return invalid_params_error(snippet) unless valid_params?
if visibility_changed?(snippet) && !visibility_allowed?(snippet, visibility_level)
@@ -14,12 +19,20 @@ module Snippets
end
update_snippet_attributes(snippet)
- spam_check(snippet, current_user, action: :update)
+
+ unless disable_spam_action_service
+ Spam::SpamActionService.new(
+ spammable: snippet,
+ request: request,
+ user: current_user,
+ action: :update
+ ).execute(spam_params: spam_params)
+ end
if save_and_commit(snippet)
Gitlab::UsageDataCounters::SnippetCounter.count(:update)
- ServiceResponse.success(payload: { snippet: snippet } )
+ ServiceResponse.success(payload: { snippet: snippet })
else
snippet_error_response(snippet, 400)
end
@@ -27,6 +40,8 @@ module Snippets
private
+ attr_reader :request, :spam_params
+
def visibility_changed?(snippet)
visibility_level && visibility_level.to_i != snippet.visibility_level
end
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index b3d617256ff..ff32bc32d93 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -4,37 +4,69 @@ module Spam
class SpamActionService
include SpamConstants
+ ##
+ # Utility method to filter SpamParams from parameters, which will later be passed to #execute
+ # after the spammable is created/updated based on the remaining parameters.
+ #
+ # Takes a hash of parameters from an incoming request to modify a model (via a controller,
+ # service, or GraphQL mutation).
+ #
+ # Deletes the parameters which are related to spam and captcha processing, and returns
+ # them in a SpamParams parameters object. See:
+ # https://refactoring.com/catalog/introduceParameterObject.html
+ def self.filter_spam_params!(params)
+ # NOTE: The 'captcha_response' field can be expanded to multiple fields when we move to future
+ # alternative captcha implementations such as FriendlyCaptcha. See
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/273480
+ captcha_response = params.delete(:captcha_response)
+
+ SpamParams.new(
+ api: params.delete(:api),
+ captcha_response: captcha_response,
+ spam_log_id: params.delete(:spam_log_id)
+ )
+ end
+
attr_accessor :target, :request, :options
attr_reader :spam_log
- def initialize(spammable:, request:, user:, context: {})
+ def initialize(spammable:, request:, user:, action:)
@target = spammable
@request = request
@user = user
- @context = context
+ @action = action
@options = {}
+ end
- if @request
- @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
- @options[:user_agent] = @request.env['HTTP_USER_AGENT']
- @options[:referrer] = @request.env['HTTP_REFERRER']
+ def execute(spam_params:)
+ if request
+ options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s
+ options[:user_agent] = request.env['HTTP_USER_AGENT']
+ options[:referrer] = request.env['HTTP_REFERRER']
else
- @options[:ip_address] = @target.ip_address
- @options[:user_agent] = @target.user_agent
+ # TODO: This code is never used, because we do not perform a verification if there is not a
+ # request. Why? Should it be deleted? Or should we check even if there is no request?
+ options[:ip_address] = target.ip_address
+ options[:user_agent] = target.user_agent
end
- end
- def execute(api: false, recaptcha_verified:, spam_log_id:)
+ recaptcha_verified = Captcha::CaptchaVerificationService.new.execute(
+ captcha_response: spam_params.captcha_response,
+ request: request
+ )
+
if recaptcha_verified
- # If it's a request which is already verified through reCAPTCHA,
+ # If it's a request which is already verified through captcha,
# update the spam log accordingly.
- SpamLog.verify_recaptcha!(user_id: user.id, id: spam_log_id)
+ SpamLog.verify_recaptcha!(user_id: user.id, id: spam_params.spam_log_id)
+ ServiceResponse.success(message: "Captcha was successfully verified")
else
- return if allowlisted?(user)
- return unless request
- return unless check_for_spam?
+ return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user)
+ return ServiceResponse.success(message: 'Skipped spam check because request was not present') unless request
+ return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam?
- perform_spam_service_check(api)
+ perform_spam_service_check(spam_params.api)
+ ServiceResponse.success(message: "Spam check performed, check #{target.class.name} spammable model for any errors or captcha requirement")
end
end
@@ -42,13 +74,27 @@ module Spam
private
- attr_reader :user, :context
+ attr_reader :user, :action
+
+ ##
+ # In order to be proceed to the spam check process, the target must be
+ # a dirty instance, which means it should be already assigned with the new
+ # attribute values.
+ def ensure_target_is_dirty
+ msg = "Target instance of #{target.class.name} must be dirty (must have changes to save)"
+ raise(msg) unless target.has_changes_to_save?
+ end
def allowlisted?(user)
user.try(:gitlab_employee?) || user.try(:gitlab_bot?) || user.try(:gitlab_service_user?)
end
+ ##
+ # Performs the spam check using the spam verdict service, and modifies the target model
+ # accordingly based on the result.
def perform_spam_service_check(api)
+ ensure_target_is_dirty
+
# since we can check for spam, and recaptcha is not verified,
# ask the SpamVerdictService what to do with the target.
spam_verdict_service.execute.tap do |result|
@@ -79,7 +125,7 @@ module Spam
description: target.spam_description,
source_ip: options[:ip_address],
user_agent: options[:user_agent],
- noteable_type: notable_type,
+ noteable_type: noteable_type,
via_api: api
}
)
@@ -88,14 +134,19 @@ module Spam
end
def spam_verdict_service
+ context = {
+ action: action,
+ target_type: noteable_type
+ }
+
SpamVerdictService.new(target: target,
user: user,
- request: @request,
+ request: request,
options: options,
- context: context.merge(target_type: notable_type))
+ context: context)
end
- def notable_type
+ def noteable_type
@notable_type ||= target.class.to_s
end
end
diff --git a/app/services/spam/spam_params.rb b/app/services/spam/spam_params.rb
new file mode 100644
index 00000000000..fef5355c7f3
--- /dev/null
+++ b/app/services/spam/spam_params.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Spam
+ ##
+ # This class is a Parameter Object (https://refactoring.com/catalog/introduceParameterObject.html)
+ # which acts as an container abstraction for multiple parameter values related to spam and
+ # captcha processing for a request.
+ #
+ # Values contained are:
+ #
+ # api: A boolean flag indicating if the request was submitted via the REST or GraphQL API
+ # captcha_response: The response resulting from the user solving a captcha. Currently it is
+ # a scalar reCAPTCHA response string, but it can be expanded to an object in the future to
+ # support other captcha implementations such as FriendlyCaptcha.
+ # spam_log_id: The id of a SpamLog record.
+ class SpamParams
+ attr_reader :api, :captcha_response, :spam_log_id
+
+ def initialize(api:, captcha_response:, spam_log_id:)
+ @api = api.present?
+ @captcha_response = captcha_response
+ @spam_log_id = spam_log_id
+ end
+
+ def ==(other)
+ other.class == self.class &&
+ other.api == self.api &&
+ other.captcha_response == self.captcha_response &&
+ other.spam_log_id == self.spam_log_id
+ end
+ end
+end
diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb
index ab80b23a37b..f9783f4271f 100644
--- a/app/services/suggestions/apply_service.rb
+++ b/app/services/suggestions/apply_service.rb
@@ -2,8 +2,9 @@
module Suggestions
class ApplyService < ::BaseService
- def initialize(current_user, *suggestions)
+ def initialize(current_user, *suggestions, message: nil)
@current_user = current_user
+ @message = message
@suggestion_set = Gitlab::Suggestions::SuggestionSet.new(suggestions)
end
@@ -30,6 +31,9 @@ module Suggestions
Suggestion.id_in(suggestion_set.suggestions)
.update_all(commit_id: result[:result], applied: true)
+
+ Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
+ .track_apply_suggestion_action(user: current_user)
end
def multi_service
@@ -44,7 +48,7 @@ module Suggestions
end
def commit_message
- Gitlab::Suggestions::CommitMessage.new(current_user, suggestion_set).message
+ Gitlab::Suggestions::CommitMessage.new(current_user, suggestion_set, @message).message
end
end
end
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index 93d2bd11426..a97c36fa0ca 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -27,6 +27,8 @@ module Suggestions
rows.in_groups_of(100, false) do |rows|
Gitlab::Database.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
end
+
+ Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_add_suggestion_action(user: @note.author)
end
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 881a139437a..5273dedb56f 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SystemHooksService
- BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember].freeze
+ BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember, Group].freeze
def execute_hooks_for(model, event)
data = build_event_data(model, event)
@@ -58,15 +58,6 @@ class SystemHooksService
end
when ProjectMember
data.merge!(project_member_data(model))
- when Group
- data.merge!(group_data(model))
-
- if event == :rename
- data.merge!(
- old_path: model.path_before_last_save,
- old_full_path: model.full_path_before_last_save
- )
- end
end
data
@@ -114,19 +105,6 @@ class SystemHooksService
}
end
- def group_data(model)
- owner = model.owner
-
- {
- name: model.name,
- path: model.path,
- full_path: model.full_path,
- group_id: model.id,
- owner_name: owner.try(:name),
- owner_email: owner.try(:email)
- }
- end
-
def user_data(model)
{
name: model.name,
@@ -141,10 +119,14 @@ class SystemHooksService
end
def builder_driven_event_data(model, event)
- case model
- when GroupMember
- Gitlab::HookData::GroupMemberBuilder.new(model).build(event)
- end
+ builder_class = case model
+ when GroupMember
+ Gitlab::HookData::GroupMemberBuilder
+ when Group
+ Gitlab::HookData::GroupBuilder
+ end
+
+ builder_class.new(model).build(event)
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 58f72e9badc..7d654ca7f5b 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -168,16 +168,19 @@ module SystemNoteService
# project - Project owning noteable
# author - User performing the change
# branch_type - 'source' or 'target'
+ # event_type - the source of event: 'update' or 'delete'
# old_branch - old branch name
# new_branch - new branch name
#
- # Example Note text:
+ # Example Note text is based on event_type:
#
- # "changed target branch from `Old` to `New`"
+ # update: "changed target branch from `Old` to `New`"
+ # delete: "changed automatically target branch to `New` because `Old` was deleted"
#
# Returns the created Note object
- def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
- ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).change_branch(branch_type, old_branch, new_branch)
+ def change_branch(noteable, project, author, branch_type, event_type, old_branch, new_branch)
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author)
+ .change_branch(branch_type, event_type, old_branch, new_branch)
end
# Called when a branch in Noteable is added or deleted
diff --git a/app/services/system_notes/merge_requests_service.rb b/app/services/system_notes/merge_requests_service.rb
index a51e2053394..99e03e67bf1 100644
--- a/app/services/system_notes/merge_requests_service.rb
+++ b/app/services/system_notes/merge_requests_service.rb
@@ -83,16 +83,26 @@ module SystemNotes
# Called when a branch in Noteable is changed
#
# branch_type - 'source' or 'target'
+ # event_type - the source of event: 'update' or 'delete'
# old_branch - old branch name
# new_branch - new branch name
+
+ # Example Note text is based on event_type:
#
- # Example Note text:
- #
- # "changed target branch from `Old` to `New`"
+ # update: "changed target branch from `Old` to `New`"
+ # delete: "changed automatically target branch to `New` because `Old` was deleted"
#
# Returns the created Note object
- def change_branch(branch_type, old_branch, new_branch)
- body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
+ def change_branch(branch_type, event_type, old_branch, new_branch)
+ body =
+ case event_type.to_s
+ when 'delete'
+ "changed automatically #{branch_type} branch to `#{new_branch}` because `#{old_branch}` was deleted"
+ when 'update'
+ "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
+ else
+ raise ArgumentError, "invalid value for event_type: #{event_type}"
+ end
create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
end
diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb
index 7e79cb9e007..9500a821071 100644
--- a/app/services/terraform/remote_state_handler.rb
+++ b/app/services/terraform/remote_state_handler.rb
@@ -68,12 +68,14 @@ module Terraform
find_params = { project: project, name: params[:name] }
- if find_only
- Terraform::State.find_by(find_params) || # rubocop: disable CodeReuse/ActiveRecord
- raise(ActiveRecord::RecordNotFound.new("Couldn't find state"))
- else
- Terraform::State.create_or_find_by(find_params)
- end
+ return find_state!(find_params) if find_only
+
+ state = Terraform::State.create_or_find_by(find_params)
+
+ # https://github.com/rails/rails/issues/36027
+ return state unless state.errors.of_kind? :name, :taken
+
+ find_state(find_params)
end
def lock_matches?(state)
@@ -86,5 +88,13 @@ module Terraform
def can_modify_state?
current_user.can?(:admin_terraform_state, project)
end
+
+ def find_state(find_params)
+ Terraform::State.find_by(find_params) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def find_state!(find_params)
+ find_state(find_params) || raise(ActiveRecord::RecordNotFound.new("Couldn't find state"))
+ end
end
end
diff --git a/app/services/test_hooks/system_service.rb b/app/services/test_hooks/system_service.rb
index 66d78bfc578..10d23421719 100644
--- a/app/services/test_hooks/system_service.rb
+++ b/app/services/test_hooks/system_service.rb
@@ -20,7 +20,8 @@ module TestHooks
end
def merge_requests_events_data
- merge_request = MergeRequest.of_projects(current_user.projects.select(:id)).first
+ merge_request = MergeRequest.of_projects(current_user.projects.select(:id)).last
+
return { error: s_('TestHooks|Ensure one of your projects has merge requests.') } unless merge_request.present?
merge_request.to_hook_data(current_user)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 12d26fe890b..dea116c8546 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -212,6 +212,11 @@ class TodoService
current_user.update_todos_count_cache
end
+ def create_request_review_todo(target, author, reviewers)
+ attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED)
+ create_todos(reviewers, attributes)
+ end
+
private
def create_todos(users, attributes)
@@ -266,8 +271,7 @@ class TodoService
def create_reviewer_todo(target, author, old_reviewers = [])
if target.reviewers.any?
reviewers = target.reviewers - old_reviewers
- attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED)
- create_todos(reviewers, attributes)
+ create_request_review_todo(target, author, reviewers)
end
end
diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb
index debd1e8cd17..fea7fc55d90 100644
--- a/app/services/users/approve_service.rb
+++ b/app/services/users/approve_service.rb
@@ -8,8 +8,7 @@ module Users
def execute(user)
return error(_('You are not allowed to approve a user'), :forbidden) unless allowed?
- return error(_('The user you are trying to approve is not pending an approval'), :conflict) if user.active?
- return error(_('The user you are trying to approve is not pending an approval'), :conflict) unless approval_required?(user)
+ return error(_('The user you are trying to approve is not pending approval'), :conflict) if user.active? || !approval_required?(user)
if user.activate
# Resends confirmation email if the user isn't confirmed yet.
@@ -18,6 +17,7 @@ module Users
user.accept_pending_invitations! if user.active_for_authentication?
DeviseMailer.user_admin_approval(user).deliver_later
+ log_event(user)
after_approve_hook(user)
success(message: 'Success', http_status: :created)
else
@@ -40,6 +40,10 @@ module Users
def approval_required?(user)
user.blocked_pending_approval?
end
+
+ def log_event(user)
+ Gitlab::AppLogger.info(message: "User instance access request approved", user: "#{user.username}", email: "#{user.email}", approved_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ end
end
end
diff --git a/app/services/users/batch_status_cleaner_service.rb b/app/services/users/batch_status_cleaner_service.rb
new file mode 100644
index 00000000000..ea6142f13cc
--- /dev/null
+++ b/app/services/users/batch_status_cleaner_service.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Users
+ class BatchStatusCleanerService
+ BATCH_SIZE = 100.freeze
+
+ # Cleanup BATCH_SIZE user_statuses records
+ # rubocop: disable CodeReuse/ActiveRecord
+ def self.execute(batch_size: BATCH_SIZE)
+ scope = UserStatus
+ .select(:user_id)
+ .scheduled_for_cleanup
+ .lock('FOR UPDATE SKIP LOCKED')
+ .limit(batch_size)
+
+ deleted_rows = UserStatus.where(user_id: scope).delete_all
+
+ { deleted_rows: deleted_rows }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index d0939d5a542..24e3fb73370 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -14,13 +14,14 @@ module Users
# service = Users::RefreshAuthorizedProjectsService.new(some_user)
# service.execute
class RefreshAuthorizedProjectsService
- attr_reader :user
+ attr_reader :user, :source
LEASE_TIMEOUT = 1.minute.to_i
# user - The User for which to refresh the authorized projects.
- def initialize(user, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil)
+ def initialize(user, source: nil, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil)
@user = user
+ @source = source
@incorrect_auth_found_callback = incorrect_auth_found_callback
@missing_auth_found_callback = missing_auth_found_callback
@@ -91,6 +92,8 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = [])
+ log_refresh_details(remove.length, add.length)
+
User.transaction do
user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty?
@@ -101,6 +104,13 @@ module Users
user.reset
end
+ def log_refresh_details(rows_deleted, rows_added)
+ Gitlab::AppJsonLogger.info(event: 'authorized_projects_refresh',
+ 'authorized_projects_refresh.source': source,
+ 'authorized_projects_refresh.rows_deleted': rows_deleted,
+ 'authorized_projects_refresh.rows_added': rows_added)
+ end
+
def fresh_access_levels_per_project
fresh_authorizations.each_with_object({}) do |row, hash|
hash[row.project_id] = row.access_level
diff --git a/app/services/users/reject_service.rb b/app/services/users/reject_service.rb
index dd72547c688..0e3eb3e5dde 100644
--- a/app/services/users/reject_service.rb
+++ b/app/services/users/reject_service.rb
@@ -12,8 +12,12 @@ module Users
user.delete_async(deleted_by: current_user, params: { hard_delete: true })
+ after_reject_hook(user)
+
NotificationService.new.user_admin_rejection(user.name, user.email)
+ log_event(user)
+
success
end
@@ -24,5 +28,15 @@ module Users
def allowed?
can?(current_user, :reject_user)
end
+
+ def after_reject_hook(user)
+ # overridden by EE module
+ end
+
+ def log_event(user)
+ Gitlab::AppLogger.info(message: "User instance access request rejected", user: "#{user.username}", email: "#{user.email}", rejected_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ end
end
end
+
+Users::RejectService.prepend_if_ee('EE::Users::RejectService')