summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/alert_management/alert.rb10
-rw-r--r--app/models/alert_management/http_integration.rb2
-rw-r--r--app/models/alerting/project_alerting_setting.rb2
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb3
-rw-r--r--app/models/analytics/cycle_analytics/project_value_stream.rb22
-rw-r--r--app/models/analytics/usage_trends/measurement.rb2
-rw-r--r--app/models/application_record.rb8
-rw-r--r--app/models/application_setting.rb70
-rw-r--r--app/models/application_setting_implementation.rb12
-rw-r--r--app/models/atlassian/identity.rb4
-rw-r--r--app/models/audit_event.rb2
-rw-r--r--app/models/blob_viewer/dependency_manager.rb2
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/board_group_recent_visit.rb20
-rw-r--r--app/models/board_project_recent_visit.rb20
-rw-r--r--app/models/broadcast_message.rb10
-rw-r--r--app/models/bulk_imports/configuration.rb4
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/bulk_imports/export.rb61
-rw-r--r--app/models/bulk_imports/export_upload.rb18
-rw-r--r--app/models/bulk_imports/file_transfer.rb20
-rw-r--r--app/models/bulk_imports/file_transfer/base_config.rb59
-rw-r--r--app/models/bulk_imports/file_transfer/group_config.rb15
-rw-r--r--app/models/bulk_imports/file_transfer/project_config.rb15
-rw-r--r--app/models/bulk_imports/stage.rb65
-rw-r--r--app/models/bulk_imports/tracker.rb2
-rw-r--r--app/models/chat_name.rb4
-rw-r--r--app/models/ci/bridge.rb5
-rw-r--r--app/models/ci/build.rb19
-rw-r--r--app/models/ci/build_dependencies.rb51
-rw-r--r--app/models/ci/build_need.rb10
-rw-r--r--app/models/ci/build_runner_session.rb3
-rw-r--r--app/models/ci/build_trace_chunk.rb3
-rw-r--r--app/models/ci/commit_with_pipeline.rb18
-rw-r--r--app/models/ci/daily_build_group_report_result.rb2
-rw-r--r--app/models/ci/deleted_object.rb2
-rw-r--r--app/models/ci/job_artifact.rb18
-rw-r--r--app/models/ci/persistent_ref.rb6
-rw-r--r--app/models/ci/pipeline.rb34
-rw-r--r--app/models/ci/pipeline_artifact.rb4
-rw-r--r--app/models/ci/pipeline_schedule.rb30
-rw-r--r--app/models/ci/processable.rb4
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/ci/runner_namespace.rb5
-rw-r--r--app/models/ci/runner_project.rb5
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/unit_test.rb1
-rw-r--r--app/models/ci/unit_test_failure.rb2
-rw-r--r--app/models/clusters/agent.rb1
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/clusters/applications/elastic_stack.rb48
-rw-r--r--app/models/clusters/applications/prometheus.rb33
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb24
-rw-r--r--app/models/clusters/concerns/application_core.rb22
-rw-r--r--app/models/clusters/concerns/elasticsearch_client.rb38
-rw-r--r--app/models/clusters/concerns/kubernetes_logger.rb27
-rw-r--r--app/models/clusters/integrations/elastic_stack.rb38
-rw-r--r--app/models/clusters/integrations/prometheus.rb38
-rw-r--r--app/models/clusters/providers/aws.rb2
-rw-r--r--app/models/commit.rb1
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb1
-rw-r--r--app/models/concerns/atomic_internal_id.rb4
-rw-r--r--app/models/concerns/board_recent_visit.rb34
-rw-r--r--app/models/concerns/cache_markdown_field.rb4
-rw-r--r--app/models/concerns/cacheable_attributes.rb4
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb26
-rw-r--r--app/models/concerns/ci/artifactable.rb2
-rw-r--r--app/models/concerns/ci/has_status.rb10
-rw-r--r--app/models/concerns/ci/maskable.rb4
-rw-r--r--app/models/concerns/ci/metadatable.rb2
-rw-r--r--app/models/concerns/cron_schedulable.rb41
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb4
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb1
-rw-r--r--app/models/concerns/enums/internal_id.rb2
-rw-r--r--app/models/concerns/enums/vulnerability.rb2
-rw-r--r--app/models/concerns/from_set_operator.rb4
-rw-r--r--app/models/concerns/group_descendant.rb4
-rw-r--r--app/models/concerns/has_integrations.rb (renamed from app/models/concerns/integration.rb)8
-rw-r--r--app/models/concerns/has_repository.rb4
-rw-r--r--app/models/concerns/has_timelogs_report.rb2
-rw-r--r--app/models/concerns/has_wiki_page_meta_attributes.rb2
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/concerns/issue_available_features.rb4
-rw-r--r--app/models/concerns/limitable.rb2
-rw-r--r--app/models/concerns/loaded_in_group_list.rb2
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/concerns/milestoneable.rb4
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb7
-rw-r--r--app/models/concerns/packages/debian/architecture.rb1
-rw-r--r--app/models/concerns/packages/debian/component.rb1
-rw-r--r--app/models/concerns/packages/debian/component_file.rb2
-rw-r--r--app/models/concerns/packages/debian/distribution.rb2
-rw-r--r--app/models/concerns/participable.rb2
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/prometheus_adapter.rb7
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/protected_ref_access.rb6
-rw-r--r--app/models/concerns/reactive_caching.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb12
-rw-r--r--app/models/concerns/repository_storage_movable.rb4
-rw-r--r--app/models/concerns/routable.rb46
-rw-r--r--app/models/concerns/services/data_fields.rb6
-rw-r--r--app/models/concerns/sha256_attribute.rb4
-rw-r--r--app/models/concerns/sha_attribute.rb6
-rw-r--r--app/models/concerns/sidebars/container_with_html_options.rb42
-rw-r--r--app/models/concerns/sidebars/has_active_routes.rb16
-rw-r--r--app/models/concerns/sidebars/has_hint.rb16
-rw-r--r--app/models/concerns/sidebars/has_icon.rb27
-rw-r--r--app/models/concerns/sidebars/has_pill.rb21
-rw-r--r--app/models/concerns/sidebars/positionable_list.rb37
-rw-r--r--app/models/concerns/sidebars/renderable.rb12
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb6
-rw-r--r--app/models/concerns/taskable.rb2
-rw-r--r--app/models/concerns/throttled_touch.rb2
-rw-r--r--app/models/concerns/timebox.rb6
-rw-r--r--app/models/concerns/token_authenticatable.rb2
-rw-r--r--app/models/concerns/triggerable_hooks.rb4
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb2
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb2
-rw-r--r--app/models/concerns/x509_serial_number_attribute.rb4
-rw-r--r--app/models/container_registry/event.rb2
-rw-r--r--app/models/container_repository.rb21
-rw-r--r--app/models/context_commits_diff.rb58
-rw-r--r--app/models/cycle_analytics/project_level_stage_adapter.rb4
-rw-r--r--app/models/deployment.rb15
-rw-r--r--app/models/deployment_merge_request.rb4
-rw-r--r--app/models/description_version.rb2
-rw-r--r--app/models/design_management/version.rb3
-rw-r--r--app/models/discussion_note.rb2
-rw-r--r--app/models/email.rb6
-rw-r--r--app/models/environment.rb10
-rw-r--r--app/models/epic.rb2
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb2
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/external_pull_request.rb4
-rw-r--r--app/models/gpg_key.rb2
-rw-r--r--app/models/group.rb70
-rw-r--r--app/models/hooks/project_hook.rb12
-rw-r--r--app/models/hooks/service_hook.rb8
-rw-r--r--app/models/hooks/web_hook.rb48
-rw-r--r--app/models/hooks/web_hook_log.rb3
-rw-r--r--app/models/hooks/web_hook_log_archived.rb12
-rw-r--r--app/models/hooks/web_hook_log_partitioned.rb17
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/identity/uniqueness_scopes.rb2
-rw-r--r--app/models/incident_management/project_incident_management_setting.rb4
-rw-r--r--app/models/instance_metadata.rb3
-rw-r--r--app/models/instance_metadata/kas.rb15
-rw-r--r--app/models/integration.rb (renamed from app/models/service.rb)140
-rw-r--r--app/models/integrations/asana.rb109
-rw-r--r--app/models/integrations/assembla.rb38
-rw-r--r--app/models/integrations/bamboo.rb183
-rw-r--r--app/models/integrations/builds_email.rb16
-rw-r--r--app/models/integrations/campfire.rb104
-rw-r--r--app/models/integrations/chat_message/alert_message.rb76
-rw-r--r--app/models/integrations/chat_message/base_message.rb88
-rw-r--r--app/models/integrations/chat_message/deployment_message.rb87
-rw-r--r--app/models/integrations/chat_message/issue_message.rb74
-rw-r--r--app/models/integrations/chat_message/merge_message.rb83
-rw-r--r--app/models/integrations/chat_message/note_message.rb86
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb267
-rw-r--r--app/models/integrations/chat_message/push_message.rb120
-rw-r--r--app/models/integrations/chat_message/wiki_page_message.rb63
-rw-r--r--app/models/integrations/confluence.rb93
-rw-r--r--app/models/integrations/datadog.rb143
-rw-r--r--app/models/integrations/emails_on_push.rb99
-rw-r--r--app/models/issue.rb30
-rw-r--r--app/models/issue/metrics.rb6
-rw-r--r--app/models/issue_assignee.rb4
-rw-r--r--app/models/issue_link.rb2
-rw-r--r--app/models/iteration.rb2
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/label_link.rb3
-rw-r--r--app/models/label_note.rb2
-rw-r--r--app/models/legacy_diff_note.rb2
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/member.rb21
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/members_preloader.rb9
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/models/merge_request/metrics.rb2
-rw-r--r--app/models/merge_request_assignee.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb4
-rw-r--r--app/models/merge_request_diff.rb40
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/milestone_release.rb2
-rw-r--r--app/models/namespace.rb25
-rw-r--r--app/models/namespace/package_setting.rb6
-rw-r--r--app/models/namespace/root_storage_statistics.rb2
-rw-r--r--app/models/namespace/traversal_hierarchy.rb24
-rw-r--r--app/models/namespace_setting.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb87
-rw-r--r--app/models/network/graph.rb2
-rw-r--r--app/models/note.rb7
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/operations/feature_flag.rb2
-rw-r--r--app/models/packages.rb2
-rw-r--r--app/models/packages/debian/group_distribution.rb10
-rw-r--r--app/models/packages/debian/project_distribution.rb5
-rw-r--r--app/models/packages/go/module.rb4
-rw-r--r--app/models/packages/go/module_version.rb12
-rw-r--r--app/models/packages/helm.rb9
-rw-r--r--app/models/packages/helm/file_metadatum.rb30
-rw-r--r--app/models/packages/package.rb40
-rw-r--r--app/models/packages/package_file.rb20
-rw-r--r--app/models/pages/lookup_path.rb11
-rw-r--r--app/models/pages/virtual_domain.rb4
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/pages_domain_acme_order.rb2
-rw-r--r--app/models/personal_access_token.rb4
-rw-r--r--app/models/plan.rb2
-rw-r--r--app/models/pool_repository.rb2
-rw-r--r--app/models/preloaders/labels_preloader.rb2
-rw-r--r--app/models/project.rb143
-rw-r--r--app/models/project_authorization.rb2
-rw-r--r--app/models/project_ci_cd_setting.rb2
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_feature_usage.rb2
-rw-r--r--app/models/project_group_link.rb3
-rw-r--r--app/models/project_import_data.rb2
-rw-r--r--app/models/project_import_state.rb2
-rw-r--r--app/models/project_services/asana_service.rb107
-rw-r--r--app/models/project_services/assembla_service.rb36
-rw-r--r--app/models/project_services/bamboo_service.rb181
-rw-r--r--app/models/project_services/bugzilla_service.rb9
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/builds_email_service.rb13
-rw-r--r--app/models/project_services/campfire_service.rb102
-rw-r--r--app/models/project_services/chat_message/alert_message.rb74
-rw-r--r--app/models/project_services/chat_message/base_message.rb86
-rw-r--r--app/models/project_services/chat_message/deployment_message.rb85
-rw-r--r--app/models/project_services/chat_message/issue_message.rb72
-rw-r--r--app/models/project_services/chat_message/merge_message.rb81
-rw-r--r--app/models/project_services/chat_message/note_message.rb84
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb265
-rw-r--r--app/models/project_services/chat_message/push_message.rb118
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb61
-rw-r--r--app/models/project_services/chat_notification_service.rb54
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_services/confluence_service.rb91
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb18
-rw-r--r--app/models/project_services/data_fields.rb6
-rw-r--r--app/models/project_services/datadog_service.rb144
-rw-r--r--app/models/project_services/emails_on_push_service.rb97
-rw-r--r--app/models/project_services/ewm_service.rb9
-rw-r--r--app/models/project_services/external_wiki_service.rb5
-rw-r--r--app/models/project_services/flowdock_service.rb13
-rw-r--r--app/models/project_services/hangouts_chat_service.rb17
-rw-r--r--app/models/project_services/hipchat_service.rb143
-rw-r--r--app/models/project_services/irker_service.rb7
-rw-r--r--app/models/project_services/issue_tracker_service.rb12
-rw-r--r--app/models/project_services/jenkins_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb13
-rw-r--r--app/models/project_services/microsoft_teams_service.rb2
-rw-r--r--app/models/project_services/monitoring_service.rb2
-rw-r--r--app/models/project_services/packagist_service.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb2
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb2
-rw-r--r--app/models/project_services/slash_commands_service.rb2
-rw-r--r--app/models/project_services/unify_circuit_service.rb2
-rw-r--r--app/models/project_services/webex_teams_service.rb17
-rw-r--r--app/models/project_services/youtrack_service.rb13
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/push_event_payload.rb2
-rw-r--r--app/models/release.rb13
-rw-r--r--app/models/release_highlight.rb36
-rw-r--r--app/models/releases/evidence.rb2
-rw-r--r--app/models/releases/link.rb2
-rw-r--r--app/models/remote_mirror.rb6
-rw-r--r--app/models/repository.rb26
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/resource_state_event.rb2
-rw-r--r--app/models/resource_timebox_event.rb2
-rw-r--r--app/models/serverless/domain_cluster.rb2
-rw-r--r--app/models/service_list.rb2
-rw-r--r--app/models/sidebars/context.rb21
-rw-r--r--app/models/sidebars/menu.rb82
-rw-r--r--app/models/sidebars/menu_item.rb21
-rw-r--r--app/models/sidebars/panel.rb75
-rw-r--r--app/models/sidebars/projects/context.rb11
-rw-r--r--app/models/sidebars/projects/menus/learn_gitlab/menu.rb41
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu.rb45
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb35
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/details.rb36
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb40
-rw-r--r--app/models/sidebars/projects/menus/repository/menu.rb59
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/branches.rb35
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/commits.rb35
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/compare.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/contributors.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/files.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/graphs.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/tags.rb28
-rw-r--r--app/models/sidebars/projects/menus/scope/menu.rb21
-rw-r--r--app/models/sidebars/projects/panel.rb26
-rw-r--r--app/models/snippet.rb13
-rw-r--r--app/models/snippet_repository.rb2
-rw-r--r--app/models/ssh_host_key.rb4
-rw-r--r--app/models/storage/legacy_project.rb2
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/terraform/state.rb2
-rw-r--r--app/models/terraform/state_version.rb2
-rw-r--r--app/models/timelog.rb15
-rw-r--r--app/models/todo.rb6
-rw-r--r--app/models/upload.rb2
-rw-r--r--app/models/user.rb68
-rw-r--r--app/models/user_callout.rb4
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/users/credit_card_validation.rb11
-rw-r--r--app/models/users/merge_request_interaction.rb2
-rw-r--r--app/models/users_statistics.rb2
-rw-r--r--app/models/vulnerability.rb2
-rw-r--r--app/models/wiki.rb15
-rw-r--r--app/models/wiki_page.rb13
331 files changed, 3785 insertions, 3630 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ba46a98b951..c18bd21d754 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_dependency 'declarative_policy'
-
class Ability
class << self
# Given a list of users and a project this method returns the users that can
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 7090d9f4ea1..156111ffaf3 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -20,7 +20,13 @@ module AlertManagement
resolved: 2,
ignored: 3
}.freeze
- private_constant :STATUSES
+
+ STATUS_DESCRIPTIONS = {
+ triggered: 'Investigation has not started',
+ acknowledged: 'Someone is actively investigating the problem',
+ resolved: 'No further work is required',
+ ignored: 'No action will be taken on the alert'
+ }.freeze
belongs_to :project
belongs_to :issue, optional: true
@@ -271,4 +277,4 @@ module AlertManagement
end
end
-AlertManagement::Alert.prepend_if_ee('EE::AlertManagement::Alert')
+AlertManagement::Alert.prepend_mod_with('AlertManagement::Alert')
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index e98c770c364..2caa9a18445 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -10,7 +10,7 @@ module AlertManagement
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
default_value_for(:endpoint_identifier, allows_nil: false) { SecureRandom.hex(8) }
diff --git a/app/models/alerting/project_alerting_setting.rb b/app/models/alerting/project_alerting_setting.rb
index 8f8c38f11e4..34fa27eb29b 100644
--- a/app/models/alerting/project_alerting_setting.rb
+++ b/app/models/alerting/project_alerting_setting.rb
@@ -10,7 +10,7 @@ module Alerting
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
before_validation :ensure_token
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
index b2c16444a2a..e8b03fa066a 100644
--- a/app/models/analytics/cycle_analytics/project_stage.rb
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -7,10 +7,13 @@ module Analytics
validates :project, presence: true
belongs_to :project
+ belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id
alias_attribute :parent, :project
alias_attribute :parent_id, :project_id
+ alias_attribute :value_stream_id, :project_value_stream_id
+
delegate :group, to: :project
validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? }
diff --git a/app/models/analytics/cycle_analytics/project_value_stream.rb b/app/models/analytics/cycle_analytics/project_value_stream.rb
new file mode 100644
index 00000000000..3eba7e87b17
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/project_value_stream.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Analytics::CycleAnalytics::ProjectValueStream < ApplicationRecord
+ belongs_to :project
+
+ has_many :stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
+
+ validates :project, :name, presence: true
+ validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :project_id }
+
+ def custom?
+ false
+ end
+
+ def stages
+ []
+ end
+
+ def self.build_default_value_stream(project)
+ new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, project: project)
+ end
+end
diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb
index ad0272699c2..46c5d56d210 100644
--- a/app/models/analytics/usage_trends/measurement.rb
+++ b/app/models/analytics/usage_trends/measurement.rb
@@ -58,4 +58,4 @@ module Analytics
end
end
-Analytics::UsageTrends::Measurement.prepend_if_ee('EE::Analytics::UsageTrends::Measurement')
+Analytics::UsageTrends::Measurement.prepend_mod_with('Analytics::UsageTrends::Measurement')
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 1bbace791ed..5e5bc00458e 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -66,6 +66,12 @@ class ApplicationRecord < ActiveRecord::Base
end
end
+ def create_or_load_association(association_name)
+ association(association_name).create unless association(association_name).loaded?
+ rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
+ association(association_name).reader
+ end
+
def self.underscore
Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
end
@@ -80,4 +86,4 @@ class ApplicationRecord < ActiveRecord::Base
end
end
-ApplicationRecord.prepend_if_ee('EE::ApplicationRecordHelpers')
+ApplicationRecord.prepend_mod_with('ApplicationRecordHelpers')
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f405f5ca5d3..65800e40d6c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,8 @@ class ApplicationSetting < ApplicationRecord
KROKI_URL_ERROR_MESSAGE = 'Please check your Kroki URL setting in ' \
'Admin Area > Settings > General > Kroki'
+ enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
+
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
@@ -132,6 +134,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :akismet_enabled
+ validates :spam_check_api_key,
+ length: { maximum: 2000, message: _('is too long (maximum is %{count} characters)') },
+ allow_blank: true
+
+ validates :spam_check_api_key,
+ presence: true,
+ if: :spam_check_endpoint_enabled
+
validates :unique_ips_limit_per_user,
numericality: { greater_than_or_equal_to: 1 },
presence: true,
@@ -365,7 +375,7 @@ class ApplicationSetting < ApplicationRecord
if: :external_authorization_service_enabled
validates :spam_check_endpoint_url,
- addressable_url: true, allow_blank: true
+ addressable_url: { schemes: %w(grpc) }, allow_blank: true
validates :spam_check_endpoint_url,
presence: true,
@@ -434,6 +444,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :throttle_unauthenticated_packages_api_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_unauthenticated_packages_api_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :throttle_authenticated_api_requests_per_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -450,6 +468,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :throttle_authenticated_packages_api_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_authenticated_packages_api_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :throttle_protected_paths_requests_per_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -475,35 +501,43 @@ class ApplicationSetting < ApplicationRecord
allow_nil: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :whats_new_variant,
+ inclusion: { in: ApplicationSetting.whats_new_variants.keys }
+
+ validates :floc_enabled,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc',
insecure_mode: true
- private_class_method def self.encryption_options_base_truncated_aes_256_gcm
+ private_class_method def self.encryption_options_base_32_aes_256_gcm
{
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: true
}
end
- attr_encrypted :external_auth_client_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :external_auth_client_key_pass, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :lets_encrypt_private_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :eks_secret_access_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :akismet_api_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :elasticsearch_aws_secret_access_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :recaptcha_private_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_truncated_aes_256_gcm
+ attr_encrypted :external_auth_client_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :external_auth_client_key_pass, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :lets_encrypt_private_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :eks_secret_access_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :akismet_api_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :spam_check_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false)
+ attr_encrypted :elasticsearch_aws_secret_access_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :elasticsearch_password, encryption_options_base_32_aes_256_gcm.merge(encode: false)
+ attr_encrypted :recaptcha_private_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :recaptcha_site_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :slack_app_secret, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :slack_app_verification_token, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :ci_jwt_signing_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :cloud_license_auth_token, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_32_aes_256_gcm
validates :disable_feed_token,
inclusion: { in: [true, false], message: _('must be a boolean value') }
@@ -634,4 +668,4 @@ class ApplicationSetting < ApplicationRecord
end
end
-ApplicationSetting.prepend_if_ee('EE::ApplicationSetting')
+ApplicationSetting.prepend_mod_with('ApplicationSetting')
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 66a8d1f8105..5ff1c653f9e 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -38,6 +38,7 @@ module ApplicationSettingImplementation
admin_mode: false,
after_sign_up_text: nil,
akismet_enabled: false,
+ akismet_api_key: nil,
allow_local_requests_from_system_hooks: true,
allow_local_requests_from_web_hooks_and_services: false,
asset_proxy_enabled: false,
@@ -76,6 +77,7 @@ module ApplicationSettingImplementation
external_pipeline_validation_service_token: nil,
external_pipeline_validation_service_url: nil,
first_day_of_week: 0,
+ floc_enabled: false,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
@@ -149,6 +151,7 @@ module ApplicationSettingImplementation
sourcegraph_url: nil,
spam_check_endpoint_enabled: false,
spam_check_endpoint_url: nil,
+ spam_check_api_key: nil,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
@@ -156,6 +159,9 @@ module ApplicationSettingImplementation
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_web_requests_per_period: 7200,
+ throttle_authenticated_packages_api_enabled: false,
+ throttle_authenticated_packages_api_period_in_seconds: 15,
+ throttle_authenticated_packages_api_requests_per_period: 1000,
throttle_incident_management_notification_enabled: false,
throttle_incident_management_notification_per_period: 3600,
throttle_incident_management_notification_period_in_seconds: 3600,
@@ -165,6 +171,9 @@ module ApplicationSettingImplementation
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_unauthenticated_requests_per_period: 3600,
+ throttle_unauthenticated_packages_api_enabled: false,
+ throttle_unauthenticated_packages_api_period_in_seconds: 15,
+ throttle_unauthenticated_packages_api_requests_per_period: 800,
time_tracking_limit_to_hours: false,
two_factor_grace_period: 48,
unique_ips_limit_enabled: false,
@@ -181,7 +190,8 @@ module ApplicationSettingImplementation
kroki_enabled: false,
kroki_url: nil,
kroki_formats: { blockdiag: false, bpmn: false, excalidraw: false },
- rate_limiting_response_text: nil
+ rate_limiting_response_text: nil,
+ whats_new_variant: 0
}
end
diff --git a/app/models/atlassian/identity.rb b/app/models/atlassian/identity.rb
index 906f2be0fbf..02bbe007e1b 100644
--- a/app/models/atlassian/identity.rb
+++ b/app/models/atlassian/identity.rb
@@ -11,14 +11,14 @@ module Atlassian
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
attr_encrypted :refresh_token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 32c9d44f836..aff7eef4622 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -92,4 +92,4 @@ class AuditEvent < ApplicationRecord
end
end
-AuditEvent.prepend_if_ee('EE::AuditEvent')
+AuditEvent.prepend_mod_with('AuditEvent')
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index a851f22bfcd..a3801025cd7 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -33,7 +33,7 @@ module BlobViewer
@json_data ||= begin
prepare!
Gitlab::Json.parse(blob.data)
- rescue
+ rescue StandardError
{}
end
end
diff --git a/app/models/board.rb b/app/models/board.rb
index b26a9461ffc..7938819b6e4 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -45,6 +45,12 @@ class Board < ApplicationRecord
def to_type
self.class.to_type
end
+
+ def disabled_for?(current_user)
+ namespace = group_board? ? resource_parent.root_ancestor : resource_parent.root_namespace
+
+ namespace.issue_repositioning_disabled? || !Ability.allowed?(current_user, :create_non_backlog_issues, self)
+ end
end
-Board.prepend_if_ee('EE::Board')
+Board.prepend_mod_with('Board')
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index 979f0e1ab92..dc273e256a8 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific group a user has visited
class BoardGroupRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :group
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :group, presence: true
validates :board, presence: true
- scope :by_user_group, -> (user, group) { where(user: user, group: group) }
-
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, group: board.group, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, group, count: nil)
- visits = by_user_group(user, group).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
+ scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
- visits.first(count)
+ def self.board_parent_relation
+ :group
end
end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 509c8f97b83..723afd6feab 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific project a user has visited
class BoardProjectRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :project
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :project, presence: true
validates :board, presence: true
- scope :by_user_project, -> (user, project) { where(user: user, project: project) }
-
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, project: board.project, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, project, count: nil)
- visits = by_user_project(user, project).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
+ scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
- visits.first(count)
+ def self.board_parent_relation
+ :project
end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index a8325e98095..1ee5c081840 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -106,6 +106,14 @@ class BroadcastMessage < ApplicationRecord
return false if current_path.blank? && target_path.present?
return true if current_path.blank? || target_path.blank?
+ # Ensure paths are consistent across callers.
+ # This fixes a mismatch between requests in the GUI and CLI
+ #
+ # This has to be reassigned due to frozen strings being provided.
+ unless current_path.start_with?("/")
+ current_path = "/#{current_path}"
+ end
+
escaped = Regexp.escape(target_path).gsub('\\*', '.*')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
@@ -119,4 +127,4 @@ class BroadcastMessage < ApplicationRecord
end
end
-BroadcastMessage.prepend_if_ee('EE::BroadcastMessage')
+BroadcastMessage.prepend_mod_with('BroadcastMessage')
diff --git a/app/models/bulk_imports/configuration.rb b/app/models/bulk_imports/configuration.rb
index 4c6f745c268..6d9f598583e 100644
--- a/app/models/bulk_imports/configuration.rb
+++ b/app/models/bulk_imports/configuration.rb
@@ -12,11 +12,11 @@ class BulkImports::Configuration < ApplicationRecord
allow_nil: true
attr_encrypted :url,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm'
attr_encrypted :access_token,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm'
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 04af1145769..bb543b39a79 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -68,6 +68,10 @@ class BulkImports::Entity < ApplicationRecord
end
end
+ def encoded_source_full_path
+ ERB::Util.url_encode(source_full_path)
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb
new file mode 100644
index 00000000000..59ca4dbfec6
--- /dev/null
+++ b/app/models/bulk_imports/export.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class Export < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+
+ self.table_name = 'bulk_import_exports'
+
+ belongs_to :project, optional: true
+ belongs_to :group, optional: true
+
+ has_one :upload, class_name: 'BulkImports::ExportUpload'
+
+ validates :project, presence: true, unless: :group
+ validates :group, presence: true, unless: :project
+ validates :relation, :status, presence: true
+
+ validate :portable_relation?
+
+ state_machine :status, initial: :started do
+ state :started, value: 0
+ state :finished, value: 1
+ state :failed, value: -1
+
+ event :start do
+ transition any => :started
+ end
+
+ event :finish do
+ transition started: :finished
+ transition failed: :failed
+ end
+
+ event :fail_op do
+ transition any => :failed
+ end
+ end
+
+ def portable_relation?
+ return unless portable
+
+ errors.add(:relation, 'Unsupported portable relation') unless config.portable_relations.include?(relation)
+ end
+
+ def portable
+ strong_memoize(:portable) do
+ project || group
+ end
+ end
+
+ def relation_definition
+ config.portable_tree[:include].find { |include| include[relation.to_sym] }
+ end
+
+ def config
+ strong_memoize(:config) do
+ FileTransfer.config_for(portable)
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/export_upload.rb b/app/models/bulk_imports/export_upload.rb
new file mode 100644
index 00000000000..a9cba5119af
--- /dev/null
+++ b/app/models/bulk_imports/export_upload.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportUpload < ApplicationRecord
+ include WithUploads
+ include ObjectStorage::BackgroundMove
+
+ self.table_name = 'bulk_import_export_uploads'
+
+ belongs_to :export, class_name: 'BulkImports::Export'
+
+ mount_uploader :export_file, ExportUploader
+
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer.rb b/app/models/bulk_imports/file_transfer.rb
new file mode 100644
index 00000000000..5be954b98da
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ extend self
+
+ UnsupportedObjectType = Class.new(StandardError)
+
+ def config_for(portable)
+ case portable
+ when ::Project
+ FileTransfer::ProjectConfig.new(portable)
+ when ::Group
+ FileTransfer::GroupConfig.new(portable)
+ else
+ raise(UnsupportedObjectType, "Unsupported object type: #{portable.class}")
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer/base_config.rb b/app/models/bulk_imports/file_transfer/base_config.rb
new file mode 100644
index 00000000000..bb04e84ad72
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer/base_config.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ class BaseConfig
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(portable)
+ @portable = portable
+ end
+
+ def portable_tree
+ attributes_finder.find_root(portable_class_sym)
+ end
+
+ def export_path
+ strong_memoize(:export_path) do
+ relative_path = File.join(base_export_path, SecureRandom.hex)
+
+ ::Gitlab::ImportExport.export_path(relative_path: relative_path)
+ end
+ end
+
+ def portable_relations
+ import_export_config.dig(:tree, portable_class_sym).keys.map(&:to_s)
+ end
+
+ private
+
+ attr_reader :portable
+
+ def attributes_finder
+ strong_memoize(:attributes_finder) do
+ ::Gitlab::ImportExport::AttributesFinder.new(config: import_export_config)
+ end
+ end
+
+ def import_export_config
+ ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h
+ end
+
+ def portable_class
+ @portable_class ||= portable.class
+ end
+
+ def portable_class_sym
+ @portable_class_sym ||= portable_class.to_s.demodulize.underscore.to_sym
+ end
+
+ def import_export_yaml
+ raise NotImplementedError
+ end
+
+ def base_export_path
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer/group_config.rb b/app/models/bulk_imports/file_transfer/group_config.rb
new file mode 100644
index 00000000000..1f845b387b8
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer/group_config.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ class GroupConfig < BaseConfig
+ def base_export_path
+ portable.full_path
+ end
+
+ def import_export_yaml
+ ::Gitlab::ImportExport.group_config_file
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb
new file mode 100644
index 00000000000..e42b5bfce3d
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer/project_config.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ class ProjectConfig < BaseConfig
+ def base_export_path
+ portable.disk_path
+ end
+
+ def import_export_yaml
+ ::Gitlab::ImportExport.config_file
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/stage.rb b/app/models/bulk_imports/stage.rb
deleted file mode 100644
index 050c2c76ce8..00000000000
--- a/app/models/bulk_imports/stage.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-module BulkImports
- class Stage
- include Singleton
-
- CONFIG = {
- group: {
- pipeline: BulkImports::Groups::Pipelines::GroupPipeline,
- stage: 0
- },
- subgroups: {
- pipeline: BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
- stage: 1
- },
- members: {
- pipeline: BulkImports::Groups::Pipelines::MembersPipeline,
- stage: 1
- },
- labels: {
- pipeline: BulkImports::Groups::Pipelines::LabelsPipeline,
- stage: 1
- },
- milestones: {
- pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline,
- stage: 1
- },
- badges: {
- pipeline: BulkImports::Groups::Pipelines::BadgesPipeline,
- stage: 1
- },
- finisher: {
- pipeline: BulkImports::Groups::Pipelines::EntityFinisher,
- stage: 2
- }
- }.freeze
-
- def self.pipelines
- instance.pipelines
- end
-
- def self.pipeline_exists?(name)
- pipelines.any? do |(_, pipeline)|
- pipeline.to_s == name.to_s
- end
- end
-
- def pipelines
- @pipelines ||= config
- .values
- .sort_by { |entry| entry[:stage] }
- .map do |entry|
- [entry[:stage], entry[:pipeline]]
- end
- end
-
- private
-
- def config
- @config ||= CONFIG
- end
- end
-end
-
-::BulkImports::Stage.prepend_if_ee('::EE::BulkImports::Stage')
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 282ba9e19ac..1b108d5c042 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -35,7 +35,7 @@ class BulkImports::Tracker < ApplicationRecord
def pipeline_class
unless BulkImports::Stage.pipeline_exists?(pipeline_name)
- raise NameError.new("'#{pipeline_name}' is not a valid BulkImport Pipeline")
+ raise NameError, "'#{pipeline_name}' is not a valid BulkImport Pipeline"
end
pipeline_name.constantize
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index 0041595baba..ff3f2663b73 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -3,11 +3,11 @@
class ChatName < ApplicationRecord
LAST_USED_AT_INTERVAL = 1.hour
- belongs_to :service
+ belongs_to :integration, foreign_key: :service_id
belongs_to :user
validates :user, presence: true
- validates :service, presence: true
+ validates :integration, presence: true
validates :team_id, presence: true
validates :chat_id, presence: true
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index ca400cebe4e..352229c64da 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -163,6 +163,9 @@ module Ci
def expanded_environment_name
end
+ def instantized_environment
+ end
+
def execute_hooks
raise NotImplementedError
end
@@ -248,4 +251,4 @@ module Ci
end
end
-::Ci::Bridge.prepend_if_ee('::EE::Ci::Bridge')
+::Ci::Bridge.prepend_mod_with('Ci::Bridge')
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3d8e9f4c126..46fc87a6ea8 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -62,6 +62,9 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
+ ignore_columns :id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
+ ignore_columns :stage_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
+
##
# Since Gitlab 11.5, deployments records started being created right after
# `ci_builds` creation. We can look up a relevant `environment` through
@@ -85,6 +88,16 @@ module Ci
end
end
+ # Initializing an object instead of fetching `persisted_environment` for avoiding unnecessary queries.
+ # We're planning to introduce a direct relationship between build and environment
+ # in https://gitlab.com/gitlab-org/gitlab/-/issues/326445 to let us to preload
+ # in batch.
+ def instantized_environment
+ return unless has_environment?
+
+ ::Environment.new(project: self.project, name: self.expanded_environment_name)
+ end
+
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
@@ -330,7 +343,7 @@ module Ci
begin
build.deployment.drop!
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id)
end
@@ -1047,7 +1060,7 @@ module Ci
end
def build_data
- @build_data ||= Gitlab::DataBuilder::Build.build(self)
+ strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
end
def successful_deployment_status
@@ -1141,4 +1154,4 @@ module Ci
end
end
-Ci::Build.prepend_if_ee('EE::Ci::Build')
+Ci::Build.prepend_mod_with('Ci::Build')
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index 8ae921f1416..716d919487d 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -14,14 +14,33 @@ module Ci
(local + cross_pipeline + cross_project).uniq
end
+ def invalid_local
+ local.reject(&:valid_dependency?)
+ end
+
+ def valid?
+ valid_local? && valid_cross_pipeline? && valid_cross_project?
+ end
+
+ private
+
+ # Dependencies can only be of Ci::Build type because only builds
+ # can create artifacts
+ def model_class
+ ::Ci::Build
+ end
+
# Dependencies local to the given pipeline
def local
- return [] if no_local_dependencies_specified?
-
- deps = model_class.where(pipeline_id: processable.pipeline_id).latest
- deps = from_previous_stages(deps)
- deps = from_needs(deps)
- from_dependencies(deps)
+ strong_memoize(:local) do
+ next [] if no_local_dependencies_specified?
+ next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline
+
+ deps = model_class.where(pipeline_id: processable.pipeline_id).latest
+ deps = from_previous_stages(deps)
+ deps = from_needs(deps)
+ from_dependencies(deps).to_a
+ end
end
# Dependencies from the same parent-pipeline hierarchy excluding
@@ -37,22 +56,6 @@ module Ci
[]
end
- def invalid_local
- local.reject(&:valid_dependency?)
- end
-
- def valid?
- valid_local? && valid_cross_pipeline? && valid_cross_project?
- end
-
- private
-
- # Dependencies can only be of Ci::Build type because only builds
- # can create artifacts
- def model_class
- ::Ci::Build
- end
-
def fetch_dependencies_in_hierarchy
deps_specifications = specified_cross_pipeline_dependencies
return [] if deps_specifications.empty?
@@ -102,8 +105,6 @@ module Ci
end
def valid_local?
- return true unless Gitlab::Ci::Features.validate_build_dependencies?(project)
-
local.all?(&:valid_dependency?)
end
@@ -154,4 +155,4 @@ module Ci
end
end
-Ci::BuildDependencies.prepend_if_ee('EE::Ci::BuildDependencies')
+Ci::BuildDependencies.prepend_mod_with('Ci::BuildDependencies')
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 7bc70f9f1e1..4a59c25cbb0 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -5,6 +5,9 @@ module Ci
extend Gitlab::Ci::Model
include BulkInsertSafe
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
@@ -14,5 +17,12 @@ module Ci
scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
scope :artifacts, -> { where(artifacts: true) }
+
+ # TODO: Remove once build_id_convert_to_bigint is not an "ignored" column anymore (see .ignore_columns above)
+ # There is a database-side trigger to populate this column. This is unexpected in the context
+ # of cloning an instance, e.g. when retrying the job. Hence we exclude the ignored column explicitly here.
+ def attributes
+ super.except('build_id_convert_to_bigint')
+ end
end
end
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index b6196048ca1..2aa856dbc64 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -5,6 +5,9 @@ module Ci
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < ApplicationRecord
extend Gitlab::Ci::Model
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7e03d709f24..719511bbb8a 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -8,6 +8,9 @@ module Ci
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::OptimisticLocking
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
diff --git a/app/models/ci/commit_with_pipeline.rb b/app/models/ci/commit_with_pipeline.rb
index 7f952fb77a0..dde4b534aaa 100644
--- a/app/models/ci/commit_with_pipeline.rb
+++ b/app/models/ci/commit_with_pipeline.rb
@@ -18,9 +18,25 @@ class Ci::CommitWithPipeline < SimpleDelegator
end
end
+ def lazy_latest_pipeline
+ BatchLoader.for(sha).batch do |shas, loader|
+ preload_pipelines = project.ci_pipelines.latest_pipeline_per_commit(shas.compact)
+
+ shas.each do |sha|
+ pipeline = preload_pipelines[sha]
+
+ loader.call(sha, pipeline)
+ end
+ end
+ end
+
def latest_pipeline(ref = nil)
@latest_pipelines.fetch(ref) do |ref|
- @latest_pipelines[ref] = latest_pipeline_for_project(ref, project)
+ @latest_pipelines[ref] = if ref
+ latest_pipeline_for_project(ref, project)
+ else
+ lazy_latest_pipeline&.itself
+ end
end
end
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index 5dcf575abd7..b46d32474c6 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -30,4 +30,4 @@ module Ci
end
end
-Ci::DailyBuildGroupReportResult.prepend_if_ee('EE::Ci::DailyBuildGroupReportResult')
+Ci::DailyBuildGroupReportResult.prepend_mod_with('Ci::DailyBuildGroupReportResult')
diff --git a/app/models/ci/deleted_object.rb b/app/models/ci/deleted_object.rb
index 2942a153e05..b2a949c9bb5 100644
--- a/app/models/ci/deleted_object.rb
+++ b/app/models/ci/deleted_object.rb
@@ -29,7 +29,7 @@ module Ci
def delete_file_from_storage
file.remove!
true
- rescue => exception
+ rescue StandardError => exception
Gitlab::ErrorTracking.track_exception(exception)
false
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 50e21a1c323..5248a80f710 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -261,6 +261,22 @@ module Ci
self.where(project: project).sum(:size)
end
+ ##
+ # FastDestroyAll concerns
+ # rubocop: disable CodeReuse/ServiceClass
+ def self.begin_fast_destroy
+ service = ::Ci::JobArtifacts::DestroyAssociationsService.new(self)
+ service.destroy_records
+ service
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
+ ##
+ # FastDestroyAll concerns
+ def self.finalize_fast_destroy(service)
+ service.update_statistics
+ end
+
def local_store?
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
@@ -331,4 +347,4 @@ module Ci
end
end
-Ci::JobArtifact.prepend_if_ee('EE::Ci::JobArtifact')
+Ci::JobArtifact.prepend_mod_with('Ci::JobArtifact')
diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb
index 91163c85a9e..57aa1962bd2 100644
--- a/app/models/ci/persistent_ref.rb
+++ b/app/models/ci/persistent_ref.rb
@@ -15,13 +15,13 @@ module Ci
def exist?
ref_exists?(path)
- rescue
+ rescue StandardError
false
end
def create
create_ref(sha, path)
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking
.track_exception(e, pipeline_id: pipeline.id)
end
@@ -30,7 +30,7 @@ module Ci
delete_refs(path)
rescue Gitlab::Git::Repository::NoRepository
# no-op
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking
.track_exception(e, pipeline_id: pipeline.id)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c9ab69317e1..f0a2c074584 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -17,6 +17,7 @@ module Ci
include FromUnion
include UpdatedAtFilterable
include EachBatch
+ include FastDestroyAll::Helpers
MAX_OPEN_MERGE_REQUESTS_REFS = 4
@@ -70,7 +71,9 @@ module Ci
has_many :deployments, through: :builds
has_many :environments, -> { distinct }, through: :deployments
has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
- has_many :downloadable_artifacts, -> { not_expired.downloadable.with_job }, through: :latest_builds, source: :job_artifacts
+ has_many :downloadable_artifacts, -> do
+ not_expired.or(where_exists(::Ci::Pipeline.artifacts_locked.where('ci_pipelines.id = ci_builds.commit_id'))).downloadable.with_job
+ end, through: :latest_builds, source: :job_artifacts
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
@@ -124,6 +127,8 @@ module Ci
after_create :keep_around_commits, unless: :importing?
+ use_fast_destroy :job_artifacts
+
# We use `Enums::Ci::Pipeline.sources` here so that EE can more easily extend
# this `Hash` with new values.
enum_with_nil source: Enums::Ci::Pipeline.sources
@@ -908,7 +913,7 @@ module Ci
def same_family_pipeline_ids
::Gitlab::Ci::PipelineObjectHierarchy.new(
- self.class.where(id: root_ancestor), options: { same_project: true }
+ self.class.default_scoped.where(id: root_ancestor), options: { same_project: true }
).base_and_descendants.select(:id)
end
@@ -1093,6 +1098,8 @@ module Ci
merge_request.modified_paths
elsif branch_updated?
push_details.modified_paths
+ elsif external_pull_request? && ::Feature.enabled?(:ci_modified_paths_of_external_prs, project, default_enabled: :yaml)
+ external_pull_request.modified_paths
end
end
end
@@ -1117,6 +1124,10 @@ module Ci
merge_request_id.present?
end
+ def external_pull_request?
+ external_pull_request_id.present?
+ end
+
def detached_merge_request_pipeline?
merge_request? && target_sha.nil?
end
@@ -1210,11 +1221,18 @@ module Ci
# We need `base_and_ancestors` in a specific order to "break" when needed.
# If we use `find_each`, then the order is broken.
# rubocop:disable Rails/FindEach
- def reset_ancestor_bridges!
- base_and_ancestors.includes(:source_bridge).each do |pipeline|
- break unless pipeline.bridge_waiting?
+ def reset_source_bridge!(current_user)
+ if ::Feature.enabled?(:ci_reset_bridge_with_subsequent_jobs, project, default_enabled: :yaml)
+ return unless bridge_waiting?
- pipeline.source_bridge.pending!
+ source_bridge.pending!
+ Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass
+ else
+ base_and_ancestors.includes(:source_bridge).each do |pipeline|
+ break unless pipeline.bridge_waiting?
+
+ pipeline.source_bridge.pending!
+ end
end
end
# rubocop:enable Rails/FindEach
@@ -1237,8 +1255,6 @@ module Ci
private
def add_message(severity, content)
- return unless Gitlab::Ci::Features.store_pipeline_messages?(project)
-
messages.build(severity: severity, content: content)
end
@@ -1294,4 +1310,4 @@ module Ci
end
end
-Ci::Pipeline.prepend_if_ee('EE::Ci::Pipeline')
+Ci::Pipeline.prepend_mod_with('Ci::Pipeline')
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 9dfe4252e95..889c5d094a7 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -40,6 +40,8 @@ module Ci
code_quality_mr_diff: 2
}
+ scope :unlocked, -> { joins(:pipeline).merge(::Ci::Pipeline.unlocked) }
+
class << self
def report_exists?(file_type)
return false unless REPORT_TYPES.key?(file_type)
@@ -58,4 +60,4 @@ module Ci
end
end
-Ci::PipelineArtifact.prepend_ee_mod
+Ci::PipelineArtifact.prepend_mod
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 3c17246bc34..9e5d517c1fe 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -5,7 +5,7 @@ module Ci
extend Gitlab::Ci::Model
include Importable
include StripAttribute
- include Schedulable
+ include CronSchedulable
include Limitable
include EachBatch
@@ -51,38 +51,16 @@ module Ci
update_attribute(:active, false)
end
- ##
- # The `next_run_at` column is set to the actual execution date of `PipelineScheduleWorker`.
- # This way, a schedule like `*/1 * * * *` won't be triggered in a short interval
- # when PipelineScheduleWorker runs irregularly by Sidekiq Memory Killer.
- def set_next_run_at
- now = Time.zone.now
- ideal_next_run = ideal_next_run_from(now)
-
- self.next_run_at = if ideal_next_run == cron_worker_next_run_from(now)
- ideal_next_run
- else
- cron_worker_next_run_from(ideal_next_run)
- end
- end
-
def job_variables
variables&.map(&:to_runner_variable) || []
end
private
- def ideal_next_run_from(start_time)
- Gitlab::Ci::CronParser.new(cron, cron_timezone)
- .next_time_from(start_time)
- end
-
- def cron_worker_next_run_from(start_time)
- Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'],
- Time.zone.name)
- .next_time_from(start_time)
+ def worker_cron_expression
+ Settings.cron_jobs['pipeline_schedule_worker']['cron']
end
end
end
-Ci::PipelineSchedule.prepend_if_ee('EE::Ci::PipelineSchedule')
+Ci::PipelineSchedule.prepend_mod_with('Ci::PipelineSchedule')
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 3b61840805a..15c57550159 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -120,6 +120,10 @@ module Ci
raise NotImplementedError
end
+ def instantized_environment
+ raise NotImplementedError
+ end
+
override :all_met_to_become_pending?
def all_met_to_become_pending?
super && !with_resource_group?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 05126853e0f..8c877c2b818 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -39,16 +39,16 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
- AVAILABLE_STATUSES = %w[active paused online offline].freeze
+ AVAILABLE_STATUSES = %w[active paused online offline not_connected].freeze
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
MINUTES_COST_FACTOR_FIELDS = %i[public_projects_minutes_cost_factor private_projects_minutes_cost_factor].freeze
has_many :builds
- has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
- has_many :runner_namespaces, inverse_of: :runner
+ has_many :runner_namespaces, inverse_of: :runner, autosave: true
has_many :groups, through: :runner_namespaces
has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
@@ -65,6 +65,7 @@ module Ci
# did `contacted_at <= ?` the query would effectively have to do a seq
# scan.
scope :offline, -> { where.not(id: online) }
+ scope :not_connected, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) }
@@ -405,4 +406,4 @@ module Ci
end
end
-Ci::Runner.prepend_if_ee('EE::Ci::Runner')
+Ci::Runner.prepend_mod_with('Ci::Runner')
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index e6c1899c89d..f819dda207d 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -3,6 +3,11 @@
module Ci
class RunnerNamespace < ApplicationRecord
extend Gitlab::Ci::Model
+ include Limitable
+
+ self.limit_name = 'ci_registered_group_runners'
+ self.limit_scope = :group
+ self.limit_feature_flag = :ci_runner_limits
belongs_to :runner, inverse_of: :runner_namespaces
belongs_to :namespace, inverse_of: :runner_namespaces, class_name: '::Namespace'
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index f5bd50dc5a3..c26b8183b52 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -3,6 +3,11 @@
module Ci
class RunnerProject < ApplicationRecord
extend Gitlab::Ci::Model
+ include Limitable
+
+ self.limit_name = 'ci_registered_project_runners'
+ self.limit_scope = :project
+ self.limit_feature_flag = :ci_runner_limits
belongs_to :runner, inverse_of: :runner_projects
belongs_to :project, inverse_of: :runner_projects
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 5ae97dcd495..ef920b2d589 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -41,7 +41,7 @@ module Ci
self.position = statuses.select(:stage_idx)
.where.not(stage_idx: nil)
.group(:stage_idx)
- .order('COUNT(*) DESC')
+ .order('COUNT(id) DESC')
.first&.stage_idx.to_i
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 85cb3f5b46a..6e27abb9f5b 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -37,4 +37,4 @@ module Ci
end
end
-Ci::Trigger.prepend_if_ee('EE::Ci::Trigger')
+Ci::Trigger.prepend_mod_with('Ci::Trigger')
diff --git a/app/models/ci/unit_test.rb b/app/models/ci/unit_test.rb
index 81623b4f6ad..9fddd9c6002 100644
--- a/app/models/ci/unit_test.rb
+++ b/app/models/ci/unit_test.rb
@@ -14,6 +14,7 @@ module Ci
belongs_to :project
scope :by_project_and_keys, -> (project, keys) { where(project_id: project.id, key_hash: keys) }
+ scope :deletable, -> { where('NOT EXISTS (?)', Ci::UnitTestFailure.select(1).where("#{Ci::UnitTestFailure.table_name}.unit_test_id = #{table_name}.id")) }
class << self
def find_or_create_by_batch(project, unit_test_attrs)
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index 653a56bd2b3..480f9cefb8e 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -11,6 +11,8 @@ module Ci
belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+ scope :deletable, -> { where('failed_at < ?', REPORT_WINDOW.ago) }
+
def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current)
joins(:unit_test)
.where(
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index c5b9dddb1da..9fb8cd024c5 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -8,6 +8,7 @@ module Clusters
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
+ has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index d42279502c5..27a3cd8d13d 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -6,7 +6,7 @@ module Clusters
include TokenAuthenticatable
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
- cached_attr_reader :last_contacted_at
+ cached_attr_reader :last_used_at
self.table_name = 'cluster_agent_tokens'
@@ -21,6 +21,8 @@ module Clusters
validates :description, length: { maximum: 1024 }
validates :name, presence: true, length: { maximum: 255 }
+ scope :order_last_used_at_desc, -> { order(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
+
def track_usage
track_values = { last_used_at: Time.current.utc }
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
index db18a29ec84..73c731aab1a 100644
--- a/app/models/clusters/applications/elastic_stack.rb
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -3,9 +3,9 @@
module Clusters
module Applications
class ElasticStack < ApplicationRecord
- VERSION = '3.0.0'
+ include ::Clusters::Concerns::ElasticsearchClient
- ELASTICSEARCH_PORT = 9200
+ VERSION = '3.0.0'
self.table_name = 'clusters_applications_elastic_stacks'
@@ -13,10 +13,23 @@ module Clusters
include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
- include ::Gitlab::Utils::StrongMemoize
default_value_for :version, VERSION
+ after_destroy do
+ cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
+ end
+
+ state_machine :status do
+ after_transition any => [:installed] do |application|
+ application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: true, chart_version: application.version)
+ end
+
+ after_transition any => [:uninstalled] do |application|
+ application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
+ end
+ end
+
def chart
'elastic-stack/elastic-stack'
end
@@ -51,31 +64,6 @@ module Clusters
super.merge('wait-for-elasticsearch.sh': File.read("#{Rails.root}/vendor/elastic_stack/wait-for-elasticsearch.sh"))
end
- def elasticsearch_client(timeout: nil)
- strong_memoize(:elasticsearch_client) do
- next unless kube_client
-
- proxy_url = kube_client.proxy_url('service', service_name, ::Clusters::Applications::ElasticStack::ELASTICSEARCH_PORT, Gitlab::Kubernetes::Helm::NAMESPACE)
-
- Elasticsearch::Client.new(url: proxy_url) do |faraday|
- # ensures headers containing auth data are appended to original client options
- faraday.headers.merge!(kube_client.headers)
- # ensure TLS certs are properly verified
- faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
- faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
- faraday.options.timeout = timeout unless timeout.nil?
- end
-
- rescue Kubeclient::HttpError => error
- # If users have mistakenly set parameters or removed the depended clusters,
- # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
- # We check for a nil client in downstream use and behaviour is equivalent to an empty state
- log_exception(error, :failed_to_create_elasticsearch_client)
-
- nil
- end
- end
-
def chart_above_v2?
Gem::Version.new(version) >= Gem::Version.new('2.0.0')
end
@@ -106,10 +94,6 @@ module Clusters
]
end
- def kube_client
- cluster&.kubeclient&.core_client
- end
-
def migrate_to_3_script
return [] if !updating? || chart_above_v3?
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index b9c136abab4..21f7e410843 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -22,21 +22,18 @@ module Clusters
attr_encrypted :alert_manager_token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
+ default_value_for(:alert_manager_token) { SecureRandom.hex }
+
after_destroy do
- run_after_commit do
- disable_prometheus_integration
- end
+ cluster.find_or_build_integration_prometheus.destroy
end
state_machine :status do
after_transition any => [:installed, :externally_installed] do |application|
- application.run_after_commit do
- Clusters::Applications::ActivateServiceWorker
- .perform_async(application.cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
- end
+ application.cluster.find_or_build_integration_prometheus.update(enabled: true, alert_manager_token: application.alert_manager_token)
end
after_transition any => :updating do |application|
@@ -44,6 +41,10 @@ module Clusters
end
end
+ def managed_prometheus?
+ !externally_installed? && !uninstalled?
+ end
+
def updated_since?(timestamp)
last_update_started_at &&
last_update_started_at > timestamp &&
@@ -70,6 +71,7 @@ module Clusters
)
end
+ # Deprecated, to be removed in %14.0 as part of https://gitlab.com/groups/gitlab-org/-/epics/4280
def patch_command(values)
helm_command_module::PatchCommand.new(
name: name,
@@ -98,23 +100,8 @@ module Clusters
files.merge('values.yaml': replaced_values)
end
- def generate_alert_manager_token!
- unless alert_manager_token.present?
- update!(alert_manager_token: generate_token)
- end
- end
-
private
- def generate_token
- SecureRandom.hex
- end
-
- def disable_prometheus_integration
- ::Clusters::Applications::DeactivateServiceWorker
- .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
- end
-
def install_knative_metrics
return [] unless cluster.application_knative_available?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index bc80bcd0b06..e8d56072b89 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.27.0'
+ VERSION = '0.28.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index a1e2aa194a0..4877ced795c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -52,6 +52,7 @@ module Clusters
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :integration_prometheus, class_name: 'Clusters::Integrations::Prometheus', inverse_of: :cluster
+ has_one :integration_elastic_stack, class_name: 'Clusters::Integrations::ElasticStack', inverse_of: :cluster
def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateName
application = APPLICATIONS[name.to_s]
@@ -104,6 +105,7 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
delegate :available?, to: :application_elastic_stack, prefix: true, allow_nil: true
+ delegate :available?, to: :integration_elastic_stack, prefix: true, allow_nil: true
delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
@@ -284,6 +286,10 @@ module Clusters
integration_prometheus || build_integration_prometheus
end
+ def find_or_build_integration_elastic_stack
+ integration_elastic_stack || build_integration_elastic_stack
+ end
+
def provider
if gcp?
provider_gcp
@@ -318,6 +324,22 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes?
end
+ def elastic_stack_adapter
+ application_elastic_stack || integration_elastic_stack
+ end
+
+ def elasticsearch_client
+ elastic_stack_adapter&.elasticsearch_client
+ end
+
+ def elastic_stack_available?
+ if application_elastic_stack_available? || integration_elastic_stack_available?
+ true
+ else
+ false
+ end
+ end
+
def kubernetes_namespace_for(environment, deployable: environment.last_deployable)
if deployable && environment.project_id != deployable.project_id
raise ArgumentError, 'environment.project_id must match deployable.project_id'
@@ -470,4 +492,4 @@ module Clusters
end
end
-Clusters::Cluster.prepend_if_ee('EE::Clusters::Cluster')
+Clusters::Cluster.prepend_mod_with('Clusters::Cluster')
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index ad6699daa78..2e40689a650 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -6,6 +6,8 @@ module Clusters
extend ActiveSupport::Concern
included do
+ include ::Clusters::Concerns::KubernetesLogger
+
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
validates :cluster, presence: true
@@ -79,27 +81,9 @@ module Clusters
# Override if your application needs any action after
# being uninstalled by Helm
end
-
- def logger
- @logger ||= Gitlab::Kubernetes::Logger.build
- end
-
- def log_exception(error, event)
- logger.error({
- exception: error.class.name,
- status_code: error.error_code,
- cluster_id: cluster&.id,
- application_id: id,
- class_name: self.class.name,
- event: event,
- message: error.message
- })
-
- Gitlab::ErrorTracking.track_exception(error, cluster_id: cluster&.id, application_id: id)
- end
end
end
end
end
-Clusters::Concerns::ApplicationCore.prepend_if_ee('EE::Clusters::Concerns::ApplicationCore')
+Clusters::Concerns::ApplicationCore.prepend_mod_with('Clusters::Concerns::ApplicationCore')
diff --git a/app/models/clusters/concerns/elasticsearch_client.rb b/app/models/clusters/concerns/elasticsearch_client.rb
new file mode 100644
index 00000000000..7b0b6bdae02
--- /dev/null
+++ b/app/models/clusters/concerns/elasticsearch_client.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Concerns
+ module ElasticsearchClient
+ include ::Gitlab::Utils::StrongMemoize
+
+ ELASTICSEARCH_PORT = 9200
+ ELASTICSEARCH_NAMESPACE = 'gitlab-managed-apps'
+
+ def elasticsearch_client(timeout: nil)
+ strong_memoize(:elasticsearch_client) do
+ kube_client = cluster&.kubeclient&.core_client
+ next unless kube_client
+
+ proxy_url = kube_client.proxy_url('service', service_name, ELASTICSEARCH_PORT, ELASTICSEARCH_NAMESPACE)
+
+ Elasticsearch::Client.new(url: proxy_url) do |faraday|
+ # ensures headers containing auth data are appended to original client options
+ faraday.headers.merge!(kube_client.headers)
+ # ensure TLS certs are properly verified
+ faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
+ faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
+ faraday.options.timeout = timeout unless timeout.nil?
+ end
+
+ rescue Kubeclient::HttpError => error
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # We check for a nil client in downstream use and behaviour is equivalent to an empty state
+ log_exception(error, :failed_to_create_elasticsearch_client)
+
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/concerns/kubernetes_logger.rb b/app/models/clusters/concerns/kubernetes_logger.rb
new file mode 100644
index 00000000000..2eca33a7610
--- /dev/null
+++ b/app/models/clusters/concerns/kubernetes_logger.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Concerns
+ module KubernetesLogger
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
+
+ def log_exception(error, event)
+ logger.error(
+ {
+ exception: error.class.name,
+ status_code: error.error_code,
+ cluster_id: cluster&.id,
+ application_id: id,
+ class_name: self.class.name,
+ event: event,
+ message: error.message
+ }
+ )
+
+ Gitlab::ErrorTracking.track_exception(error, cluster_id: cluster&.id, application_id: id)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/integrations/elastic_stack.rb b/app/models/clusters/integrations/elastic_stack.rb
new file mode 100644
index 00000000000..565d268259a
--- /dev/null
+++ b/app/models/clusters/integrations/elastic_stack.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Integrations
+ class ElasticStack < ApplicationRecord
+ include ::Clusters::Concerns::ElasticsearchClient
+ include ::Clusters::Concerns::KubernetesLogger
+
+ self.table_name = 'clusters_integration_elasticstack'
+ self.primary_key = :cluster_id
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ validates :cluster, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+
+ def available?
+ enabled
+ end
+
+ def service_name
+ chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
+ end
+
+ def chart_above_v2?
+ return true if chart_version.nil?
+
+ Gem::Version.new(chart_version) >= Gem::Version.new('2.0.0')
+ end
+
+ def chart_above_v3?
+ return true if chart_version.nil?
+
+ Gem::Version.new(chart_version) >= Gem::Version.new('3.0.0')
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index 1496d8ff1dd..0a01ac5d1ce 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -4,6 +4,7 @@ module Clusters
module Integrations
class Prometheus < ApplicationRecord
include ::Clusters::Concerns::PrometheusClient
+ include AfterCommitQueue
self.table_name = 'clusters_integration_prometheus'
self.primary_key = :cluster_id
@@ -13,9 +14,46 @@ module Clusters
validates :cluster, presence: true
validates :enabled, inclusion: { in: [true, false] }
+ attr_encrypted :alert_manager_token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm'
+
+ default_value_for(:alert_manager_token) { SecureRandom.hex }
+
+ after_destroy do
+ run_after_commit do
+ deactivate_project_services
+ end
+ end
+
+ after_save do
+ next unless enabled_before_last_save != enabled
+
+ run_after_commit do
+ if enabled
+ activate_project_services
+ else
+ deactivate_project_services
+ end
+ end
+ end
+
def available?
enabled?
end
+
+ private
+
+ def activate_project_services
+ ::Clusters::Applications::ActivateServiceWorker
+ .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
+ end
+
+ def deactivate_project_services
+ ::Clusters::Applications::DeactivateServiceWorker
+ .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
+ end
end
end
end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index bfd01775620..af2eba42721 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -18,7 +18,7 @@ module Clusters
attr_encrypted :secret_access_key,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
validates :role_arn,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5c3e3685c64..09e43bb8f20 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -142,6 +142,7 @@ class Commit
delegate \
:pipelines,
:last_pipeline,
+ :lazy_latest_pipeline,
:latest_pipeline,
:latest_pipeline_for_project,
:set_latest_pipeline_for_ref,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e989129209a..c5ba19438cd 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -214,8 +214,14 @@ class CommitStatus < ApplicationRecord
allow_failure? && (failed? || canceled?)
end
+ # Time spent running.
def duration
- calculate_duration
+ calculate_duration(started_at, finished_at)
+ end
+
+ # Time spent in the pending state.
+ def queued_duration
+ calculate_duration(queued_at, started_at)
end
def latest?
@@ -286,4 +292,4 @@ class CommitStatus < ApplicationRecord
end
end
-CommitStatus.prepend_if_ee('::EE::CommitStatus')
+CommitStatus.prepend_mod_with('CommitStatus')
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index f1c39dda49d..90d48aa81d0 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -27,6 +27,7 @@ module Analytics
scope :default_stages, -> { where(custom: false) }
scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
+ scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
end
def parent=(_)
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index bbf9ecbcfe9..80cf6260b0b 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -214,9 +214,9 @@ module AtomicInternalId
def self.project_init(klass, column_name = :iid)
->(instance, scope) do
if instance
- klass.where(project_id: instance.project_id).maximum(column_name)
+ klass.default_scoped.where(project_id: instance.project_id).maximum(column_name)
elsif scope.present?
- klass.where(**scope).maximum(column_name)
+ klass.default_scoped.where(**scope).maximum(column_name)
end
end
end
diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb
new file mode 100644
index 00000000000..fd4d574ac58
--- /dev/null
+++ b/app/models/concerns/board_recent_visit.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module BoardRecentVisit
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def visited!(user, board)
+ find_or_create_by(
+ "user" => user,
+ board_parent_relation => board.resource_parent,
+ board_relation => board
+ ).tap do |visit|
+ visit.touch
+ end
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def latest(user, parent, count: nil)
+ visits = by_user_parent(user, parent).order(updated_at: :desc)
+ visits = visits.preload(board_relation)
+
+ visits.first(count)
+ end
+
+ def board_relation
+ :board
+ end
+
+ def board_parent_relation
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 34c1b6d25a4..a5cf947ba07 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -26,7 +26,7 @@ module CacheMarkdownField
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
- raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ raise ArgumentError, "Unknown field: #{field.inspect}" unless
cached_markdown_fields.markdown_fields.include?(field)
# Always include a project key, or Banzai complains
@@ -99,7 +99,7 @@ module CacheMarkdownField
end
def cached_html_for(markdown_field)
- raise ArgumentError.new("Unknown field: #{markdown_field}") unless
+ raise ArgumentError, "Unknown field: #{markdown_field}" unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index ee56322cce7..f3b47047c55 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -53,7 +53,7 @@ module CacheableAttributes
return cached_record if cached_record.present?
current_without_cache.tap { |current_record| current_record&.cache! }
- rescue => e
+ rescue StandardError => e
if Rails.env.production?
Gitlab::AppLogger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}")
else
@@ -66,7 +66,7 @@ module CacheableAttributes
def expire
Gitlab::SafeRequestStore.delete(request_store_cache_key)
cache_backend.delete(cache_key)
- rescue
+ rescue StandardError
# Gracefully handle when Redis is not available. For example,
# omnibus may fail here during gitlab:assets:compile.
end
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index 2b4a108a9a0..9efd90756b1 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -55,6 +55,7 @@ module CascadingNamespaceSettingAttribute
# public methods
define_attr_reader(attribute)
define_attr_writer(attribute)
+ define_lock_attr_writer(attribute)
define_lock_methods(attribute)
alias_boolean(attribute)
@@ -84,7 +85,7 @@ module CascadingNamespaceSettingAttribute
next self[attribute] unless self.class.cascading_settings_feature_enabled?
next self[attribute] if will_save_change_to_attribute?(attribute)
- next locked_value(attribute) if cascading_attribute_locked?(attribute)
+ next locked_value(attribute) if cascading_attribute_locked?(attribute, include_self: false)
next self[attribute] unless self[attribute].nil?
cascaded_value = cascaded_ancestor_value(attribute)
@@ -97,15 +98,25 @@ module CascadingNamespaceSettingAttribute
def define_attr_writer(attribute)
define_method("#{attribute}=") do |value|
+ return value if value == cascaded_ancestor_value(attribute)
+
clear_memoization(attribute)
+ super(value)
+ end
+ end
+
+ def define_lock_attr_writer(attribute)
+ define_method("lock_#{attribute}=") do |value|
+ attr_value = public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ write_attribute(attribute, attr_value) if self[attribute].nil?
super(value)
end
end
def define_lock_methods(attribute)
- define_method("#{attribute}_locked?") do
- cascading_attribute_locked?(attribute)
+ define_method("#{attribute}_locked?") do |include_self: false|
+ cascading_attribute_locked?(attribute, include_self: include_self)
end
define_method("#{attribute}_locked_by_ancestor?") do
@@ -133,7 +144,7 @@ module CascadingNamespaceSettingAttribute
def define_validator_methods(attribute)
define_method("#{attribute}_changeable?") do
return unless cascading_attribute_changed?(attribute)
- return unless cascading_attribute_locked?(attribute)
+ return unless cascading_attribute_locked?(attribute, include_self: false)
errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
end
@@ -141,7 +152,7 @@ module CascadingNamespaceSettingAttribute
define_method("lock_#{attribute}_changeable?") do
return unless cascading_attribute_changed?("lock_#{attribute}")
- if cascading_attribute_locked?(attribute)
+ if cascading_attribute_locked?(attribute, include_self: false)
return errors.add(:"lock_#{attribute}", s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
end
@@ -202,8 +213,9 @@ module CascadingNamespaceSettingAttribute
Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend
end
- def cascading_attribute_locked?(attribute)
- locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
+ def cascading_attribute_locked?(attribute, include_self:)
+ locked_by_self = include_self ? public_send("lock_#{attribute}?") : false # rubocop:disable GitlabSecurity/PublicSend
+ locked_by_self || locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
end
def cascading_attribute_changed?(attribute)
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 0d29955268f..27040a677ff 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -43,4 +43,4 @@ module Ci
end
end
-Ci::Artifactable.prepend_ee_mod
+Ci::Artifactable.prepend_mod
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index c990da5873a..f3c254053b5 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -122,12 +122,10 @@ module Ci
private
- def calculate_duration
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.current - started_at
- end
+ def calculate_duration(start_time, end_time)
+ return unless start_time
+
+ (end_time || Time.current) - start_time
end
end
end
diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb
index 4e0ee72f18f..e1ef4531845 100644
--- a/app/models/concerns/ci/maskable.rb
+++ b/app/models/concerns/ci/maskable.rb
@@ -9,9 +9,9 @@ module Ci
# * No variables
# * No spaces
# * Minimal length of 8 characters
- # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'
+ # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~'
# * Absolutely no fun is allowed
- REGEX = /\A[a-zA-Z0-9_+=\/@:.-]{8,}\z/.freeze
+ REGEX = /\A[a-zA-Z0-9_+=\/@:.~-]{8,}\z/.freeze
included do
validates :masked, inclusion: { in: [true, false] }
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 26e644646b4..601637ea32a 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -88,4 +88,4 @@ module Ci
end
end
-Ci::Metadatable.prepend_if_ee('EE::Ci::Metadatable')
+Ci::Metadatable.prepend_mod_with('Ci::Metadatable')
diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb
new file mode 100644
index 00000000000..beb3a09c119
--- /dev/null
+++ b/app/models/concerns/cron_schedulable.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module CronSchedulable
+ extend ActiveSupport::Concern
+ include Schedulable
+
+ ##
+ # The `next_run_at` column is set to the actual execution date of worker that
+ # triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
+ # in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
+ def set_next_run_at
+ now = Time.zone.now
+ ideal_next_run = ideal_next_run_from(now)
+
+ self.next_run_at = if ideal_next_run == cron_worker_next_run_from(now)
+ ideal_next_run
+ else
+ cron_worker_next_run_from(ideal_next_run)
+ end
+ end
+
+ private
+
+ def ideal_next_run_from(start_time)
+ next_time_from(start_time, cron, cron_timezone)
+ end
+
+ def cron_worker_next_run_from(start_time)
+ next_time_from(start_time, worker_cron_expression, Time.zone.name)
+ end
+
+ def next_time_from(start_time, cron, cron_timezone)
+ Gitlab::Ci::CronParser
+ .new(cron, cron_timezone)
+ .next_time_from(start_time)
+ end
+
+ def worker_cron_expression
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index de17f50cd29..2e368b12cb7 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -22,6 +22,8 @@ module Enums
forward_deployment_failure: 13,
user_blocked: 14,
project_deleted: 15,
+ ci_quota_exceeded: 16,
+ pipeline_loop_detected: 17,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
@@ -35,4 +37,4 @@ module Enums
end
end
-Enums::Ci::CommitStatus.prepend_if_ee('EE::Enums::Ci::CommitStatus')
+Enums::Ci::CommitStatus.prepend_mod_with('Enums::Ci::CommitStatus')
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index fdc48d09db2..c42b046592f 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -10,6 +10,7 @@ module Enums
unknown_failure: 0,
config_error: 1,
external_validation_failure: 2,
+ user_not_verified: 3,
activity_limit_exceeded: 20,
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22,
diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb
index b08c05b1934..71c86bab136 100644
--- a/app/models/concerns/enums/internal_id.rb
+++ b/app/models/concerns/enums/internal_id.rb
@@ -22,4 +22,4 @@ module Enums
end
end
-Enums::InternalId.prepend_if_ee('EE::Enums::InternalId')
+Enums::InternalId.prepend_mod_with('Enums::InternalId')
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
index 4b2e9e9e0b2..55360eb92e6 100644
--- a/app/models/concerns/enums/vulnerability.rb
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -43,4 +43,4 @@ module Enums
end
end
-Enums::Vulnerability.prepend_if_ee('EE::Enums::Vulnerability')
+Enums::Vulnerability.prepend_mod_with('Enums::Vulnerability')
diff --git a/app/models/concerns/from_set_operator.rb b/app/models/concerns/from_set_operator.rb
index 593fd251c5c..c6d63631c84 100644
--- a/app/models/concerns/from_set_operator.rb
+++ b/app/models/concerns/from_set_operator.rb
@@ -10,8 +10,8 @@ module FromSetOperator
raise "Trying to redefine method '#{method(method_name)}'" if methods.include?(method_name)
- define_method(method_name) do |members, remove_duplicates: true, alias_as: table_name|
- operator_sql = operator.new(members, remove_duplicates: remove_duplicates).to_sql
+ define_method(method_name) do |members, remove_duplicates: true, remove_order: true, alias_as: table_name|
+ operator_sql = operator.new(members, remove_duplicates: remove_duplicates, remove_order: remove_order).to_sql
from(Arel.sql("(#{operator_sql}) #{alias_as}"))
end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 67953105bed..b376537a418 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -22,7 +22,7 @@ module GroupDescendant
return [] if descendants.empty?
unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) }
- raise ArgumentError.new(_('element is not a hierarchy'))
+ raise ArgumentError, _('element is not a hierarchy')
end
all_hierarchies = descendants.map do |descendant|
@@ -56,7 +56,7 @@ module GroupDescendant
end
if parent.nil? && hierarchy_top.present?
- raise ArgumentError.new(_('specified top is not part of the tree'))
+ raise ArgumentError, _('specified top is not part of the tree')
end
if parent && parent != hierarchy_top
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/has_integrations.rb
index 5e53f13be95..b2775f4cbb2 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/has_integrations.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-module Integration
+module HasIntegrations
extend ActiveSupport::Concern
class_methods do
def with_custom_integration_for(integration, page = nil, per = nil)
- custom_integration_project_ids = Service
+ custom_integration_project_ids = Integration
.select(:project_id)
.where(type: integration.type)
.where(inherit_from_id: nil)
@@ -17,13 +17,13 @@ module Integration
end
def without_integration(integration)
- services = Service
+ integrations = Integration
.select('1')
.where('services.project_id = projects.id')
.where(type: integration.type)
Project
- .where('NOT EXISTS (?)', services)
+ .where('NOT EXISTS (?)', integrations)
.where(pending_delete: false)
.where(archived: false)
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 774cda2c3e8..33f6904bc91 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -17,7 +17,7 @@ module HasRepository
def valid_repo?
repository.exists?
- rescue
+ rescue StandardError
errors.add(:base, _('Invalid repository path'))
false
end
@@ -25,7 +25,7 @@ module HasRepository
def repo_exists?
strong_memoize(:repo_exists) do
repository.exists?
- rescue
+ rescue StandardError
false
end
end
diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb
index 90f9876de95..3af063438bf 100644
--- a/app/models/concerns/has_timelogs_report.rb
+++ b/app/models/concerns/has_timelogs_report.rb
@@ -15,6 +15,6 @@ module HasTimelogsReport
private
def timelogs_for(start_time, end_time)
- Timelog.between_times(start_time, end_time).for_issues_in_group(self)
+ Timelog.between_times(start_time, end_time).in_group(self)
end
end
diff --git a/app/models/concerns/has_wiki_page_meta_attributes.rb b/app/models/concerns/has_wiki_page_meta_attributes.rb
index 136f2d00ce3..55681bc91a5 100644
--- a/app/models/concerns/has_wiki_page_meta_attributes.rb
+++ b/app/models/concerns/has_wiki_page_meta_attributes.rb
@@ -59,7 +59,7 @@ module HasWikiPageMetaAttributes
if conflict.present?
meta.errors.add(:canonical_slug, 'Duplicate value found')
- raise CanonicalSlugConflictError.new(meta)
+ raise CanonicalSlugConflictError, meta
end
meta
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 1e44321e148..f5c70f10dc5 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -63,7 +63,7 @@ module Issuable
has_many :note_authors, -> { distinct }, through: :notes, source: :author
- has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
+ has_many :label_links, as: :target, inverse_of: :target
has_many :labels, through: :label_links
has_many :todos, as: :target
@@ -103,7 +103,7 @@ module Issuable
end
scope :assigned_to, ->(u) do
assignees_table = Arel::Table.new("#{to_ability_name}_assignees")
- sql = assignees_table.project('true').where(assignees_table[:user_id].in(u)).where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ sql = assignees_table.project('true').where(assignees_table[:user_id].in(u.id)).where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
where("EXISTS (#{sql.to_sql})")
end
# rubocop:enable GitlabSecurity/SqlInjection
@@ -564,4 +564,4 @@ module Issuable
end
end
-Issuable.prepend_if_ee('EE::Issuable')
+Issuable.prepend_mod_with('Issuable')
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index a5ffa959174..28d12a033a6 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -29,5 +29,5 @@ module IssueAvailableFeatures
end
end
-IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
-IssueAvailableFeatures::ClassMethods.prepend_if_ee('EE::IssueAvailableFeatures::ClassMethods')
+IssueAvailableFeatures.prepend_mod_with('IssueAvailableFeatures')
+IssueAvailableFeatures::ClassMethods.prepend_mod_with('IssueAvailableFeatures::ClassMethods')
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index 3cb0bd85936..672bcdbbb1b 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -7,6 +7,7 @@ module Limitable
included do
class_attribute :limit_scope
class_attribute :limit_name
+ class_attribute :limit_feature_flag
self.limit_name = self.name.demodulize.tableize
validate :validate_plan_limit_not_exceeded, on: :create
@@ -25,6 +26,7 @@ module Limitable
def validate_scoped_plan_limit_not_exceeded
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
+ return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation, default_enabled: :yaml)
relation = self.class.where(limit_scope => scope_relation)
limits = scope_relation.actual_limits
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index 59e0ed75d2d..848ef63f1c2 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -79,4 +79,4 @@ module LoadedInGroupList
end
end
-LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods')
+LoadedInGroupList::ClassMethods.prepend_mod_with('LoadedInGroupList::ClassMethods')
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 5db077c178d..f1baa923ec5 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -211,4 +211,4 @@ module Mentionable
end
end
-Mentionable.prepend_if_ee('EE::Mentionable')
+Mentionable.prepend_mod_with('Mentionable')
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index 5a5ce1809d0..e33b6db0103 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -37,4 +37,4 @@ module Mentionable
end
end
-Mentionable::ReferenceRegexes.prepend_if_ee('EE::Mentionable::ReferenceRegexes')
+Mentionable::ReferenceRegexes.prepend_mod_with('Mentionable::ReferenceRegexes')
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index d42417bb6c1..c4f810ab9b1 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -28,7 +28,7 @@ module Milestoneable
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
- .where('milestone_releases.release_id IS NULL')
+ .where(milestone_releases: { release_id: nil })
end
scope :joins_milestone_releases, -> do
@@ -57,4 +57,4 @@ module Milestoneable
end
end
-Milestoneable.prepend_if_ee('EE::Milestoneable')
+Milestoneable.prepend_mod_with('Milestoneable')
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index f3cc68e4b85..f6d4e5bd27b 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -183,5 +183,5 @@ end
Noteable.extend(Noteable::ClassMethods)
-Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods')
-Noteable.prepend_if_ee('EE::Noteable')
+Noteable::ClassMethods.prepend_mod_with('Noteable::ClassMethods')
+Noteable.prepend_mod_with('Noteable')
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
index c7af841e450..19d2ac620f3 100644
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ b/app/models/concerns/optimized_issuable_label_filter.rb
@@ -28,7 +28,6 @@ module OptimizedIssuableLabelFilter
# Taken from IssuableFinder
def count_by_state
- return super if root_namespace.nil?
return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
count_params = params.merge(state: nil, sort: nil, force_cte: true)
@@ -40,7 +39,11 @@ module OptimizedIssuableLabelFilter
.group(:state_id)
.count
- counts = state_counts.transform_keys { |key| count_key(key) }
+ counts = Hash.new(0)
+
+ state_counts.each do |key, value|
+ counts[count_key(key)] += value
+ end
counts[:all] = counts.values.sum
counts.with_indifferent_access
diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb
index 760ebb49980..e2fa0ceb0f6 100644
--- a/app/models/concerns/packages/debian/architecture.rb
+++ b/app/models/concerns/packages/debian/architecture.rb
@@ -23,6 +23,7 @@ module Packages
uniqueness: { scope: %i[distribution_id] },
format: { with: Gitlab::Regex.debian_architecture_regex }
+ scope :ordered_by_name, -> { order(:name) }
scope :with_distribution, ->(distribution) { where(distribution: distribution) }
scope :with_name, ->(name) { where(name: name) }
end
diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb
index 7b342c7b684..5ea686faec2 100644
--- a/app/models/concerns/packages/debian/component.rb
+++ b/app/models/concerns/packages/debian/component.rb
@@ -23,6 +23,7 @@ module Packages
uniqueness: { scope: %i[distribution_id] },
format: { with: Gitlab::Regex.debian_component_regex }
+ scope :ordered_by_name, -> { order(:name) }
scope :with_distribution, ->(distribution) { where(distribution: distribution) }
scope :with_name, ->(name) { where(name: name) }
end
diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb
index 3cc2c291e96..c41635a0d16 100644
--- a/app/models/concerns/packages/debian/component_file.rb
+++ b/app/models/concerns/packages/debian/component_file.rb
@@ -60,6 +60,8 @@ module Packages
scope :preload_distribution, -> { includes(component: :distribution) }
+ scope :created_before, ->(reference) { where("#{table_name}.created_at < ?", reference) }
+
mount_file_store_uploader Packages::Debian::ComponentFileUploader
before_validation :update_size_from_file
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 08fb9ccf3ea..267c7a4d201 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -84,7 +84,7 @@ module Packages
attr_encrypted :signing_keys,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index acd654bd229..25410a859e9 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -135,4 +135,4 @@ module Participable
end
end
-Participable.prepend_if_ee('EE::Participable')
+Participable.prepend_mod_with('Participable')
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 7c774d8bad7..484c91e0833 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -109,4 +109,4 @@ module ProjectFeaturesCompatibility
end
end
-ProjectFeaturesCompatibility.prepend_if_ee('EE::ProjectFeaturesCompatibility')
+ProjectFeaturesCompatibility.prepend_mod_with('ProjectFeaturesCompatibility')
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index 55c2bf96a94..afebc426762 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -26,9 +26,14 @@ module PrometheusAdapter
}
end
+ # Overridden in app/models/clusters/applications/prometheus.rb
+ def managed_prometheus?
+ false
+ end
+
# This is a light-weight check if a prometheus client is properly configured.
def configured?
- raise NotImplemented
+ raise NotImplementedError
end
# This is a heavy-weight check if a prometheus is properly configured and accessible from GitLab.
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 2828ae4a3a9..ec56f4a32af 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -78,4 +78,4 @@ end
# since these are defined in a ClassMethods constant. As such, we prepend the
# module directly into ProtectedRef::ClassMethods, instead of prepending it into
# ProtectedRef.
-ProtectedRef::ClassMethods.prepend_if_ee('EE::ProtectedRef')
+ProtectedRef::ClassMethods.prepend_mod_with('ProtectedRef')
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 5e38ce7cad8..618ad96905d 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -53,12 +53,12 @@ module ProtectedRefAccess
end
end
-ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes')
-ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess')
+ProtectedRefAccess.include_mod_with('ProtectedRefAccess::Scopes')
+ProtectedRefAccess.prepend_mod_with('ProtectedRefAccess')
# When using `prepend` (or `include` for that matter), the `ClassMethods`
# constants are not merged. This means that `class_methods` in
# `EE::ProtectedRefAccess` would be ignored.
#
# To work around this, we prepend the `ClassMethods` constant manually.
-ProtectedRefAccess::ClassMethods.prepend_if_ee('EE::ProtectedRefAccess::ClassMethods')
+ProtectedRefAccess::ClassMethods.prepend_mod_with('ProtectedRefAccess::ClassMethods')
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index dbc70ac2218..9ed2070d11c 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -168,7 +168,7 @@ module ReactiveCaching
data_deep_size = Gitlab::Utils::DeepSize.new(data, max_size: self.class.reactive_cache_hard_limit)
- raise ExceededReactiveCacheLimit.new unless data_deep_size.valid?
+ raise ExceededReactiveCacheLimit unless data_deep_size.valid?
end
end
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 7f559f0a7ed..75dfed6d58f 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -53,13 +53,13 @@ module RelativePositioning
return [size, starting_from] if size >= MIN_GAP
+ terminus = context.at_position(starting_from)
+
if at_end
- terminus = context.max_sibling
terminus.shift_left
max_relative_position = terminus.relative_position
[[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
else
- terminus = context.min_sibling
terminus.shift_right
min_relative_position = terminus.relative_position
[[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
@@ -79,6 +79,8 @@ module RelativePositioning
objects = objects.reject(&:relative_position)
return 0 if objects.empty?
+ objects.first.check_repositioning_allowed!
+
number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each
representative = RelativePositioning.mover.context(objects.first)
@@ -123,6 +125,12 @@ module RelativePositioning
::Gitlab::RelativePositioning::Mover.new(START_POSITION, (MIN_POSITION..MAX_POSITION))
end
+ # To be overriden on child classes whenever
+ # blocking position updates is necessary.
+ def check_repositioning_allowed!
+ nil
+ end
+
def move_between(before, after)
before, after = [before, after].sort_by(&:relative_position) if before && after
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 8607f0d94f4..1dd8eebeff3 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -50,7 +50,7 @@ module RepositoryStorageMovable
begin
storage_move.container.set_repository_read_only!(skip_git_transfer_check: true)
- rescue => err
+ rescue StandardError => err
storage_move.add_error(err.message)
next false
end
@@ -114,7 +114,7 @@ module RepositoryStorageMovable
private
def container_repository_writable
- add_error(_('is read only')) if container&.repository_read_only?
+ add_error(_('is read-only')) if container&.repository_read_only?
end
def error_key
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 71d8e06de76..847abdc1b6d 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -96,11 +96,49 @@ module Routable
end
def full_name
- route&.name || build_full_name
+ # We have to test for persistence as the cache key uses #updated_at
+ return (route&.name || build_full_name) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml)
+
+ # Return the name as-is if the parent is missing
+ return name if route.nil? && parent.nil? && name.present?
+
+ # If the route is already preloaded, return directly, preventing an extra load
+ return route.name if route_loaded? && route.present?
+
+ # Similarly, we can allow the build if the parent is loaded
+ return build_full_name if parent_loaded?
+
+ Gitlab::Cache.fetch_once([cache_key, :full_name]) do
+ route&.name || build_full_name
+ end
end
def full_path
- route&.path || build_full_path
+ # We have to test for persistence as the cache key uses #updated_at
+ return (route&.path || build_full_path) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml)
+
+ # Return the path as-is if the parent is missing
+ return path if route.nil? && parent.nil? && path.present?
+
+ # If the route is already preloaded, return directly, preventing an extra load
+ return route.path if route_loaded? && route.present?
+
+ # Similarly, we can allow the build if the parent is loaded
+ return build_full_path if parent_loaded?
+
+ Gitlab::Cache.fetch_once([cache_key, :full_path]) do
+ route&.path || build_full_path
+ end
+ end
+
+ # Overriden in the Project model
+ # parent_id condition prevents issues with parent reassignment
+ def parent_loaded?
+ association(:parent).loaded?
+ end
+
+ def route_loaded?
+ association(:route).loaded?
end
def full_path_components
@@ -124,7 +162,9 @@ module Routable
def set_path_errors
route_path_errors = self.errors.delete(:"route.path")
- self.errors[:path].concat(route_path_errors) if route_path_errors
+ route_path_errors&.each do |msg|
+ self.errors.add(:path, msg)
+ end
end
def full_name_changed?
diff --git a/app/models/concerns/services/data_fields.rb b/app/models/concerns/services/data_fields.rb
index 10963e4e7d8..fd56af449bc 100644
--- a/app/models/concerns/services/data_fields.rb
+++ b/app/models/concerns/services/data_fields.rb
@@ -5,11 +5,11 @@ module Services
extend ActiveSupport::Concern
included do
- belongs_to :service
+ belongs_to :integration, inverse_of: self.name.underscore.to_sym, foreign_key: :service_id
- delegate :activated?, to: :service, allow_nil: true
+ delegate :activated?, to: :integration, allow_nil: true
- validates :service, presence: true
+ validates :integration, presence: true
end
class_methods do
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
index 9dfe1b77829..4921f7f1a7e 100644
--- a/app/models/concerns/sha256_attribute.rb
+++ b/app/models/concerns/sha256_attribute.rb
@@ -31,9 +31,9 @@ module Sha256Attribute
end
unless column.type == :binary
- raise ArgumentError.new("sha256_attribute #{name.inspect} is invalid since the column type is not :binary")
+ raise ArgumentError, "sha256_attribute #{name.inspect} is invalid since the column type is not :binary"
end
- rescue => error
+ rescue StandardError => error
Gitlab::AppLogger.error "Sha256Attribute initialization: #{error.message}"
raise
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index cbac6a210c7..f6f5dbce4b6 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -24,9 +24,9 @@ module ShaAttribute
return unless column
unless column.type == :binary
- raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary")
+ raise ArgumentError, "sha_attribute #{name.inspect} is invalid since the column type is not :binary"
end
- rescue => error
+ rescue StandardError => error
Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
raise
end
@@ -37,4 +37,4 @@ module ShaAttribute
end
end
-ShaAttribute::ClassMethods.prepend_if_ee('EE::ShaAttribute')
+ShaAttribute::ClassMethods.prepend_mod_with('ShaAttribute')
diff --git a/app/models/concerns/sidebars/container_with_html_options.rb b/app/models/concerns/sidebars/container_with_html_options.rb
deleted file mode 100644
index 12ea366c66a..00000000000
--- a/app/models/concerns/sidebars/container_with_html_options.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module ContainerWithHtmlOptions
- # The attributes returned from this method
- # will be applied to helper methods like
- # `link_to` or the div containing the container.
- def container_html_options
- {
- aria: { label: title }
- }.merge(extra_container_html_options)
- end
-
- # Classes will override mostly this method
- # and not `container_html_options`.
- def extra_container_html_options
- {}
- end
-
- # Attributes to pass to the html_options attribute
- # in the helper method that sets the active class
- # on each element.
- def nav_link_html_options
- {}
- end
-
- def title
- raise NotImplementedError
- end
-
- # The attributes returned from this method
- # will be applied right next to the title,
- # for example in the span that renders the title.
- def title_html_options
- {}
- end
-
- def link
- raise NotImplementedError
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_active_routes.rb b/app/models/concerns/sidebars/has_active_routes.rb
deleted file mode 100644
index e7a153f067a..00000000000
--- a/app/models/concerns/sidebars/has_active_routes.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module HasActiveRoutes
- # This method will indicate for which paths or
- # controllers, the menu or menu item should
- # be set as active.
- #
- # The returned values are passed to the `nav_link` helper method,
- # so the params can be either `path`, `page`, `controller`.
- # Param 'action' is not supported.
- def active_routes
- {}
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_hint.rb b/app/models/concerns/sidebars/has_hint.rb
deleted file mode 100644
index 21dca39dca0..00000000000
--- a/app/models/concerns/sidebars/has_hint.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# This module has the necessary methods to store
-# hints for menus. Hints are elements displayed
-# when the user hover the menu item.
-module Sidebars
- module HasHint
- def show_hint?
- false
- end
-
- def hint_html_options
- {}
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_icon.rb b/app/models/concerns/sidebars/has_icon.rb
deleted file mode 100644
index d1a87918285..00000000000
--- a/app/models/concerns/sidebars/has_icon.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# This module has the necessary methods to show
-# sprites or images next to the menu item.
-module Sidebars
- module HasIcon
- def sprite_icon
- nil
- end
-
- def sprite_icon_html_options
- {}
- end
-
- def image_path
- nil
- end
-
- def image_html_options
- {}
- end
-
- def icon_or_image?
- sprite_icon || image_path
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_pill.rb b/app/models/concerns/sidebars/has_pill.rb
deleted file mode 100644
index ad7064fe63d..00000000000
--- a/app/models/concerns/sidebars/has_pill.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# This module introduces the logic to show the "pill" element
-# next to the menu item, indicating the a count.
-module Sidebars
- module HasPill
- def has_pill?
- false
- end
-
- # In this method we will need to provide the query
- # to retrieve the elements count
- def pill_count
- raise NotImplementedError
- end
-
- def pill_html_options
- {}
- end
- end
-end
diff --git a/app/models/concerns/sidebars/positionable_list.rb b/app/models/concerns/sidebars/positionable_list.rb
deleted file mode 100644
index 30830d547f3..00000000000
--- a/app/models/concerns/sidebars/positionable_list.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-# This module handles elements in a list. All elements
-# must have a different class
-module Sidebars
- module PositionableList
- def add_element(list, element)
- list << element
- end
-
- def insert_element_before(list, before_element, new_element)
- index = index_of(list, before_element)
-
- if index
- list.insert(index, new_element)
- else
- list.unshift(new_element)
- end
- end
-
- def insert_element_after(list, after_element, new_element)
- index = index_of(list, after_element)
-
- if index
- list.insert(index + 1, new_element)
- else
- add_element(list, new_element)
- end
- end
-
- private
-
- def index_of(list, element)
- list.index { |e| e.is_a?(element) }
- end
- end
-end
diff --git a/app/models/concerns/sidebars/renderable.rb b/app/models/concerns/sidebars/renderable.rb
deleted file mode 100644
index a3976af8515..00000000000
--- a/app/models/concerns/sidebars/renderable.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Renderable
- # This method will control whether the menu or menu_item
- # should be rendered. It will be overriden by specific
- # classes.
- def render?
- true
- end
- end
-end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index a82cf338039..948190dfadf 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -10,7 +10,7 @@ module Storage
proj_with_tags = first_project_with_container_registry_tags
if proj_with_tags
- raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry")
+ raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry"
end
parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
@@ -48,7 +48,7 @@ module Storage
begin
send_update_instructions
write_projects_repository_config
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
full_path_before_last_save: full_path_before_last_save,
full_path: full_path,
@@ -83,7 +83,7 @@ module Storage
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ raise Gitlab::UpdatePathError, 'namespace directory cannot be moved'
end
end
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index d8867177059..4d1c1d44af7 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -12,7 +12,7 @@ module Taskable
COMPLETED = 'completed'
INCOMPLETE = 'incomplete'
COMPLETE_PATTERN = /(\[[xX]\])/.freeze
- INCOMPLETE_PATTERN = /(\[[\s]\])/.freeze
+ INCOMPLETE_PATTERN = /(\[\s\])/.freeze
ITEM_PATTERN = %r{
^
(?:(?:>\s{0,4})*) # optional blockquote characters
diff --git a/app/models/concerns/throttled_touch.rb b/app/models/concerns/throttled_touch.rb
index 797c46f6cc5..b5682abb229 100644
--- a/app/models/concerns/throttled_touch.rb
+++ b/app/models/concerns/throttled_touch.rb
@@ -6,7 +6,7 @@ module ThrottledTouch
# The amount of time to wait before "touch" can update a record again.
TOUCH_INTERVAL = 1.minute
- def touch(*args)
+ def touch(*args, **kwargs)
super if (Time.zone.now - updated_at) > TOUCH_INTERVAL
end
end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 8273059b30c..fb9a8cd312d 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -72,11 +72,7 @@ module Timebox
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
- if Feature.enabled?(:optimized_timebox_queries, default_enabled: true)
- from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
- else
- where(project_id: projects).or(where(group_id: groups))
- end
+ from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
end
# A timebox is within the timeframe (start_date, end_date) if it overlaps
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 535cf25eb9d..34c8630bb90 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -12,7 +12,7 @@ module TokenAuthenticatable
def add_authentication_token_field(token_field, options = {})
if token_authenticatable_fields.include?(token_field)
- raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
+ raise ArgumentError, "#{token_field} already configured via add_authentication_token_field"
end
token_authenticatable_fields.push(token_field)
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index db5df6c2c9f..8fe34632430 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -29,11 +29,11 @@ module TriggerableHooks
callable_scopes = triggers.keys + [:all]
return none unless callable_scopes.include?(trigger)
- public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
+ executable.public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
end
def select_active(hooks_scope, data)
- select do |hook|
+ executable.select do |hook|
ActiveHookFilter.new(hook).matches?(hooks_scope, data)
end
end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index cf50305faab..f0e5e010e70 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -4,4 +4,4 @@ module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
end
-VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
+VulnerabilityFindingHelpers.prepend_mod_with('VulnerabilityFindingHelpers')
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
index f57e3cb0bfb..f98c1e93aaf 100644
--- a/app/models/concerns/vulnerability_finding_signature_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -4,4 +4,4 @@ module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
end
-VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')
+VulnerabilityFindingSignatureHelpers.prepend_mod_with('VulnerabilityFindingSignatureHelpers')
diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb
index d2a5c736604..dbba80eff53 100644
--- a/app/models/concerns/x509_serial_number_attribute.rb
+++ b/app/models/concerns/x509_serial_number_attribute.rb
@@ -31,9 +31,9 @@ module X509SerialNumberAttribute
end
unless column.type == :binary
- raise ArgumentError.new("x509_serial_number_attribute #{name.inspect} is invalid since the column type is not :binary")
+ raise ArgumentError, "x509_serial_number_attribute #{name.inspect} is invalid since the column type is not :binary"
end
- rescue => error
+ rescue StandardError => error
Gitlab::AppLogger.error "X509SerialNumberAttribute initialization: #{error.message}"
raise
end
diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb
index 109fda675a2..c1b865ae578 100644
--- a/app/models/container_registry/event.rb
+++ b/app/models/container_registry/event.rb
@@ -66,4 +66,4 @@ module ContainerRegistry
end
end
-::ContainerRegistry::Event.prepend_if_ee('EE::ContainerRegistry::Event')
+::ContainerRegistry::Event.prepend_mod_with('ContainerRegistry::Event')
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index e2bdf8ffce2..6e0d0e347c9 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -7,6 +7,7 @@ class ContainerRepository < ApplicationRecord
include Sortable
WAITING_CLEANUP_STATUSES = %i[cleanup_scheduled cleanup_unfinished].freeze
+ REQUIRING_CLEANUP_STATUSES = %i[cleanup_unscheduled cleanup_scheduled].freeze
belongs_to :project
@@ -31,6 +32,7 @@ class ContainerRepository < ApplicationRecord
scope :for_project_id, ->(project_id) { where(project_id: project_id) }
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
scope :waiting_for_cleanup, -> { where(expiration_policy_cleanup_status: WAITING_CLEANUP_STATUSES) }
+ scope :expiration_policy_started_at_nil_or_before, ->(timestamp) { where('expiration_policy_started_at < ? OR expiration_policy_started_at IS NULL', timestamp) }
def self.exists_by_path?(path)
where(
@@ -39,6 +41,23 @@ class ContainerRepository < ApplicationRecord
).exists?
end
+ def self.with_enabled_policy
+ joins("INNER JOIN container_expiration_policies ON container_repositories.project_id = container_expiration_policies.project_id")
+ .where(container_expiration_policies: { enabled: true })
+ end
+
+ def self.requiring_cleanup
+ where(
+ container_repositories: { expiration_policy_cleanup_status: REQUIRING_CLEANUP_STATUSES },
+ project_id: ::ContainerExpirationPolicy.runnable_schedules
+ .select(:project_id)
+ )
+ end
+
+ def self.with_unfinished_cleanup
+ with_enabled_policy.cleanup_unfinished
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def registry
@registry ||= begin
@@ -140,4 +159,4 @@ class ContainerRepository < ApplicationRecord
end
end
-ContainerRepository.prepend_if_ee('EE::ContainerRepository')
+ContainerRepository.prepend_mod_with('ContainerRepository')
diff --git a/app/models/context_commits_diff.rb b/app/models/context_commits_diff.rb
new file mode 100644
index 00000000000..fe1a72b79f2
--- /dev/null
+++ b/app/models/context_commits_diff.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+class ContextCommitsDiff
+ include ActsAsPaginatedDiff
+
+ attr_reader :merge_request
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def empty?
+ commits.empty?
+ end
+
+ def commits_count
+ merge_request.context_commits_count
+ end
+
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::Compare.new(
+ self,
+ project: merge_request.project,
+ diff_options: diff_options,
+ diff_refs: diff_refs
+ )
+ end
+
+ def raw_diffs(options = {})
+ compare.diffs(options.merge(paths: paths))
+ end
+
+ def diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: commits.last&.diff_refs&.base_sha,
+ head_sha: commits.first&.diff_refs&.head_sha
+ )
+ end
+
+ private
+
+ def compare
+ @compare ||=
+ Gitlab::Git::Compare.new(
+ merge_request.project.repository.raw_repository,
+ commits.last&.diff_refs&.base_sha,
+ commits.first&.diff_refs&.head_sha
+ )
+ end
+
+ def commits
+ @commits ||= merge_request.project.repository.commits_by(oids: merge_request.recent_context_commits.map(&:id))
+ end
+
+ def paths
+ merge_request.merge_request_context_commit_diff_files.map(&:path)
+ end
+end
diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb
index dd4afa9b809..5538e93a39e 100644
--- a/app/models/cycle_analytics/project_level_stage_adapter.rb
+++ b/app/models/cycle_analytics/project_level_stage_adapter.rb
@@ -4,6 +4,8 @@
# compatible with the old value stream controller actions.
module CycleAnalytics
class ProjectLevelStageAdapter
+ ProjectLevelStage = Struct.new(:title, :description, :legend, :name, :project_median, keyword_init: true )
+
def initialize(stage, options)
@stage = stage
@options = options
@@ -13,7 +15,7 @@ module CycleAnalytics
def as_json(serializer: AnalyticsStageSerializer)
presenter = Analytics::CycleAnalytics::StagePresenter.new(stage)
- serializer.new.represent(OpenStruct.new(
+ serializer.new.represent(ProjectLevelStage.new(
title: presenter.title,
description: presenter.description,
legend: presenter.legend,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index d3280403bfd..e2b25690323 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -32,8 +32,9 @@ class Deployment < ApplicationRecord
delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true
scope :for_environment, -> (environment) { where(environment_id: environment) }
- scope :for_environment_name, -> (name) do
- joins(:environment).where(environments: { name: name })
+ scope :for_environment_name, -> (project, name) do
+ where('deployments.environment_id = (?)',
+ Environment.select(:id).where(project: project, name: name).limit(1))
end
scope :for_status, -> (status) { where(status: status) }
@@ -87,7 +88,7 @@ class Deployment < ApplicationRecord
after_transition any => :running do |deployment|
deployment.run_after_commit do
- Deployments::ExecuteHooksWorker.perform_async(id)
+ Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
end
@@ -100,7 +101,7 @@ class Deployment < ApplicationRecord
after_transition any => FINISHED_STATUSES do |deployment|
deployment.run_after_commit do
- Deployments::ExecuteHooksWorker.perform_async(id)
+ Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
end
@@ -182,8 +183,8 @@ class Deployment < ApplicationRecord
Commit.truncate_sha(sha)
end
- def execute_hooks
- deployment_data = Gitlab::DataBuilder::Deployment.build(self)
+ def execute_hooks(status_changed_at)
+ deployment_data = Gitlab::DataBuilder::Deployment.build(self, status_changed_at)
project.execute_hooks(deployment_data, :deployment_hooks)
project.execute_services(deployment_data, :deployment_hooks)
end
@@ -347,4 +348,4 @@ class Deployment < ApplicationRecord
end
end
-Deployment.prepend_if_ee('EE::Deployment')
+Deployment.prepend_mod_with('Deployment')
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
index 7949bd81605..b91785eeb57 100644
--- a/app/models/deployment_merge_request.rb
+++ b/app/models/deployment_merge_request.rb
@@ -12,7 +12,7 @@ class DeploymentMergeRequest < ApplicationRecord
end
def self.by_deployment_id(id)
- where('deployments.id = ?', id)
+ where(deployments: { id: id })
end
def self.deployed_to(name)
@@ -20,7 +20,7 @@ class DeploymentMergeRequest < ApplicationRecord
# (project_id, name), instead of using the index on
# (name varchar_pattern_ops). This results in better performance on
# GitLab.com.
- where('environments.name = ?', name)
+ where(environments: { name: name })
.where('environments.project_id = merge_requests.target_project_id')
end
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index f69564f4893..96c8553c101 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -29,4 +29,4 @@ class DescriptionVersion < ApplicationRecord
end
end
-DescriptionVersion.prepend_if_ee('EE::DescriptionVersion')
+DescriptionVersion.prepend_mod_with('DescriptionVersion')
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 5cfd8f3ec8e..ca65cf38f0d 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -58,6 +58,7 @@ module DesignManagement
scope :ordered, -> { order(id: :desc) }
scope :for_issue, -> (issue) { where(issue: issue) }
scope :by_sha, -> (sha) { where(sha: sha) }
+ scope :with_author, -> { includes(:author) }
# This is the one true way to create a Version.
#
@@ -94,7 +95,7 @@ module DesignManagement
version
end
- rescue
+ rescue StandardError
raise CouldNotCreateVersion.new(sha, issue_id, design_actions)
end
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index 5049107da2c..6621b30b645 100644
--- a/app/models/discussion_note.rb
+++ b/app/models/discussion_note.rb
@@ -5,7 +5,7 @@
# A note of this type can be resolvable.
class DiscussionNote < Note
# This prepend must stay here because the `validates` below depends on it.
- prepend_if_ee('EE::DiscussionNote') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ prepend_mod_with('DiscussionNote') # rubocop: disable Cop/InjectEnterpriseEditionModule
# Names of all implementers of `Noteable` that support discussions.
def self.noteable_types
diff --git a/app/models/email.rb b/app/models/email.rb
index c5154267ff0..0140f784842 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -22,7 +22,7 @@ class Email < ApplicationRecord
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
- delegate :username, :can?, to: :user
+ delegate :username, :can?, :pending_invitations, :accept_pending_invitations!, to: :user
def email=(value)
write_attribute(:email, value.downcase.strip)
@@ -32,10 +32,6 @@ class Email < ApplicationRecord
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end
- def accept_pending_invitations!
- user.accept_pending_invitations!
- end
-
def validate_email_format
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 4ee93b0ba4a..2e677a3d177 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -24,13 +24,13 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
- has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
+ has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
- has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
+ has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
@@ -269,7 +269,7 @@ class Environment < ApplicationRecord
Gitlab::OptimisticLocking.retry_lock(deployment.deployable, name: 'environment_cancel_deployment_jobs') do |deployable|
deployable.cancel! if deployable&.cancelable?
end
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, environment_id: id, deployment_id: deployment.id)
end
end
@@ -406,7 +406,7 @@ class Environment < ApplicationRecord
end
def elastic_stack_available?
- !!deployment_platform&.cluster&.application_elastic_stack_available?
+ !!deployment_platform&.cluster&.elastic_stack_available?
end
def rollout_status
@@ -471,4 +471,4 @@ class Environment < ApplicationRecord
end
end
-Environment.prepend_if_ee('EE::Environment')
+Environment.prepend_mod_with('Environment')
diff --git a/app/models/epic.rb b/app/models/epic.rb
index 93f286f97d3..81cd342576f 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -18,4 +18,4 @@ class Epic < ApplicationRecord
end
end
-Epic.prepend_if_ee('EE::Epic')
+Epic.prepend_mod_with('Epic')
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 9a9fbc6a801..956b5d6470f 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -38,7 +38,7 @@ module ErrorTracking
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
after_save :clear_reactive_cache!
diff --git a/app/models/event.rb b/app/models/event.rb
index 401dfc4cb02..5b755736f47 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -448,4 +448,4 @@ class Event < ApplicationRecord
end
end
-Event.prepend_if_ee('EE::Event')
+Event.prepend_mod_with('Event')
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
index 1487a6387f0..3fc166203e7 100644
--- a/app/models/external_pull_request.rb
+++ b/app/models/external_pull_request.rb
@@ -72,6 +72,10 @@ class ExternalPullRequest < ApplicationRecord
end
end
+ def modified_paths
+ project.repository.diff_stats(target_sha, source_sha).paths
+ end
+
private
def actual_source_branch_sha
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 330815ab8c1..0cb3662368c 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -128,4 +128,4 @@ class GpgKey < ApplicationRecord
end
end
-GpgKey.prepend_if_ee('EE::GpgKey')
+GpgKey.prepend_mod_with('GpgKey')
diff --git a/app/models/group.rb b/app/models/group.rb
index 2967c1ffc1d..da795651c63 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -34,7 +34,7 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
- has_many :services
+ has_many :integrations
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
has_many :shared_groups, through: :shared_group_links, source: :shared_group
@@ -67,6 +67,8 @@ class Group < Namespace
has_one :import_state, class_name: 'GroupImportState', inverse_of: :group
+ has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group
+
has_many :group_deploy_keys_groups, inverse_of: :group
has_many :group_deploy_keys, through: :group_deploy_keys_groups
has_many :group_deploy_tokens
@@ -105,21 +107,21 @@ class Group < Namespace
scope :with_users, -> { includes(:users) }
+ scope :with_onboarding_progress, -> { joins(:onboarding_progress) }
+
scope :by_id, ->(groups) { where(id: groups) }
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
- .where("members.user_id IN (?)", user_ids)
+ .where(members: { user_id: user_ids })
.where("access_level >= ?", Gitlab::Access::GUEST)
end
scope :for_authorized_project_members, -> (user_ids) do
joins(projects: :project_authorizations)
- .where("project_authorizations.user_id IN (?)", user_ids)
+ .where(project_authorizations: { user_id: user_ids })
end
- delegate :default_branch_name, to: :namespace_settings
-
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'
@@ -155,7 +157,7 @@ class Group < Namespace
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
- .where('project_namespace.share_with_group_lock = ?', false)
+ .where(project_namespace: { share_with_group_lock: false })
.select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
@@ -163,12 +165,12 @@ class Group < Namespace
end
def without_integration(integration)
- services = Service
+ integrations = Integration
.select('1')
.where('services.group_id = namespaces.id')
.where(type: integration.type)
- where('NOT EXISTS (?)', services)
+ where('NOT EXISTS (?)', integrations)
end
# This method can be used only if all groups have the same top-level
@@ -448,6 +450,20 @@ class Group < Namespace
.where(source_id: id)
end
+ def authorizable_members_with_parents
+ source_ids =
+ if has_parent?
+ self_and_ancestors.reorder(nil).select(:id)
+ else
+ id
+ end
+
+ group_hierarchy_members = GroupMember.where(source_id: source_ids)
+
+ GroupMember.from_union([group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares]).authorizable
+ end
+
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
@@ -553,11 +569,22 @@ class Group < Namespace
def max_member_access_for_user(user, only_concrete_membership: false)
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership
+ # Use the preloaded value that exists instead of performing the db query again(cached or not).
+ # Groups::GroupMembersController#preload_max_access makes use of this by
+ # calling Group#max_member_access. This helps when we have a process
+ # that may query this multiple times from the outside through a policy query
+ # like the GroupPolicy#lookup_access_level! does as a condition for any role
+ return user.max_access_for_group[id] if user.max_access_for_group[id]
+
+ max_member_access(user)
+ end
- max_member_access = members_with_parents.where(user_id: user)
- .reorder(access_level: :desc)
- .first
- &.access_level
+ def max_member_access(user)
+ max_member_access = members_with_parents
+ .where(user_id: user)
+ .reorder(access_level: :desc)
+ .first
+ &.access_level
max_member_access || GroupMember::NO_ACCESS
end
@@ -622,7 +649,7 @@ class Group < Namespace
end
def access_request_approvers_to_be_notified
- members.owners.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ members.owners.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def supports_events?
@@ -693,6 +720,14 @@ class Group < Namespace
Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
+ def to_ability_name
+ model_name.singular
+ end
+
+ def activity_path
+ Gitlab::Routing.url_helpers.activity_group_path(self)
+ end
+
private
def update_two_factor_requirement
@@ -820,7 +855,12 @@ class Group < Namespace
end
def uncached_ci_variables_for(ref, project, environment: nil)
- list_of_ids = [self] + ancestors
+ list_of_ids = if root_ancestor.use_traversal_ids?
+ [self] + ancestors(hierarchy_order: :asc)
+ else
+ [self] + ancestors
+ end
+
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)
@@ -835,4 +875,4 @@ class Group < Namespace
end
end
-Group.prepend_if_ee('EE::Group')
+Group.prepend_mod_with('Group')
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b625a70b444..a28b97e63e5 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -4,6 +4,7 @@ class ProjectHook < WebHook
include TriggerableHooks
include Presentable
include Limitable
+ extend ::Gitlab::Utils::Override
self.limit_scope = :project
@@ -29,6 +30,15 @@ class ProjectHook < WebHook
def pluralized_name
_('Webhooks')
end
+
+ def web_hooks_disable_failed?
+ Feature.enabled?(:web_hooks_disable_failed, project)
+ end
+
+ override :rate_limit
+ def rate_limit
+ project.actual_limits.limit_for(:web_hook_calls)
+ end
end
-ProjectHook.prepend_if_ee('EE::ProjectHook')
+ProjectHook.prepend_mod_with('ProjectHook')
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 4caa45a13d4..1a466b333a5 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -3,12 +3,10 @@
class ServiceHook < WebHook
include Presentable
- belongs_to :service
- validates :service, presence: true
+ belongs_to :integration, foreign_key: :service_id
+ validates :integration, presence: true
- # rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name = 'service_hook')
- WebHookService.new(self, data, hook_name).execute
+ super(data, hook_name)
end
- # rubocop: enable CodeReuse/ServiceClass
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index dbd5a1b032a..02b4feb4ccc 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,6 +3,11 @@
class WebHook < ApplicationRecord
include Sortable
+ FAILURE_THRESHOLD = 3 # three strikes
+ INITIAL_BACKOFF = 10.minutes
+ MAX_BACKOFF = 1.day
+ BACKOFF_GROWTH_FACTOR = 2.0
+
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -21,15 +26,27 @@ class WebHook < ApplicationRecord
validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true
+ scope :executable, -> do
+ next all unless Feature.enabled?(:web_hooks_disable_failed)
+
+ where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
+ end
+
+ def executable?
+ return true unless web_hooks_disable_failed?
+
+ recent_failures <= FAILURE_THRESHOLD && (disabled_until.nil? || disabled_until < Time.current)
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name)
- WebHookService.new(self, data, hook_name).execute
+ WebHookService.new(self, data, hook_name).execute if executable?
end
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
def async_execute(data, hook_name)
- WebHookService.new(self, data, hook_name).async_execute
+ WebHookService.new(self, data, hook_name).async_execute if executable?
end
# rubocop: enable CodeReuse/ServiceClass
@@ -41,4 +58,31 @@ class WebHook < ApplicationRecord
def help_path
'user/project/integrations/webhooks'
end
+
+ def next_backoff
+ return MAX_BACKOFF if backoff_count >= 8 # optimization to prevent expensive exponentiation and possible overflows
+
+ (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**backoff_count))
+ .clamp(INITIAL_BACKOFF, MAX_BACKOFF)
+ .seconds
+ end
+
+ def disable!
+ update!(recent_failures: FAILURE_THRESHOLD + 1)
+ end
+
+ def enable!
+ update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ end
+
+ # Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
+ def rate_limit
+ nil
+ end
+
+ private
+
+ def web_hooks_disable_failed?
+ Feature.enabled?(:web_hooks_disable_failed)
+ end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index e2230a2d644..0c96d5d4b6d 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -5,9 +5,12 @@ class WebHookLog < ApplicationRecord
include Presentable
include DeleteWithLimit
include CreatedAtFilterable
+ include PartitionedTable
self.primary_key = :id
+ partitioned_by :created_at, strategy: :monthly
+
belongs_to :web_hook
serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/hooks/web_hook_log_archived.rb b/app/models/hooks/web_hook_log_archived.rb
new file mode 100644
index 00000000000..a1c8a44f5ba
--- /dev/null
+++ b/app/models/hooks/web_hook_log_archived.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# This model is not intended to be used.
+# It is a temporary reference to the old non-partitioned
+# web_hook_logs table.
+# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5558
+# for details.
+# rubocop:disable Gitlab/NamespacedClass: This is a temporary class with no relevant namespace
+# WebHook, WebHookLog and all hooks are defined outside of a namespace
+class WebHookLogArchived < ApplicationRecord
+ self.table_name = 'web_hook_logs_archived'
+end
diff --git a/app/models/hooks/web_hook_log_partitioned.rb b/app/models/hooks/web_hook_log_partitioned.rb
deleted file mode 100644
index b4b150afb6a..00000000000
--- a/app/models/hooks/web_hook_log_partitioned.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-# This model is not yet intended to be used.
-# It is in a transitioning phase while we are partitioning
-# the web_hook_logs table on the database-side.
-# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5558
-# for details.
-# rubocop:disable Gitlab/NamespacedClass: This is a temporary class with no relevant namespace
-# WebHook, WebHookLog and all hooks are defined outside of a namespace
-class WebHookLogPartitioned < ApplicationRecord
- include PartitionedTable
-
- self.table_name = 'web_hook_logs_part_0c5294f417'
- self.primary_key = :id
-
- partitioned_by :created_at, strategy: :monthly
-end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index fc97c68b756..df1185f330d 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -51,4 +51,4 @@ class Identity < ApplicationRecord
end
end
-Identity.prepend_if_ee('EE::Identity')
+Identity.prepend_mod_with('Identity')
diff --git a/app/models/identity/uniqueness_scopes.rb b/app/models/identity/uniqueness_scopes.rb
index c1890865a1c..b41b4572e82 100644
--- a/app/models/identity/uniqueness_scopes.rb
+++ b/app/models/identity/uniqueness_scopes.rb
@@ -10,4 +10,4 @@ class Identity < ApplicationRecord
end
end
-Identity::UniquenessScopes.prepend_if_ee('EE::Identity::UniquenessScopes')
+Identity::UniquenessScopes.prepend_mod_with('Identity::UniquenessScopes')
diff --git a/app/models/incident_management/project_incident_management_setting.rb b/app/models/incident_management/project_incident_management_setting.rb
index 4887265be88..b6da93508c2 100644
--- a/app/models/incident_management/project_incident_management_setting.rb
+++ b/app/models/incident_management/project_incident_management_setting.rb
@@ -12,7 +12,7 @@ module IncidentManagement
attr_encrypted :pagerduty_token,
mode: :per_attribute_iv,
- key: ::Settings.attr_encrypted_db_key_base_truncated,
+ key: ::Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false, # No need to encode for binary column https://github.com/attr-encrypted/attr_encrypted#the-encode-encode_iv-encode_salt-and-default_encoding-options
encode_iv: false
@@ -52,4 +52,4 @@ module IncidentManagement
end
end
-IncidentManagement::ProjectIncidentManagementSetting.prepend_if_ee('EE::IncidentManagement::ProjectIncidentManagementSetting')
+IncidentManagement::ProjectIncidentManagementSetting.prepend_mod_with('IncidentManagement::ProjectIncidentManagementSetting')
diff --git a/app/models/instance_metadata.rb b/app/models/instance_metadata.rb
index 96622d0b1b3..6cac78178e0 100644
--- a/app/models/instance_metadata.rb
+++ b/app/models/instance_metadata.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
class InstanceMetadata
- attr_reader :version, :revision
+ attr_reader :version, :revision, :kas
def initialize(version: Gitlab::VERSION, revision: Gitlab.revision)
@version = version
@revision = revision
+ @kas = ::InstanceMetadata::Kas.new
end
end
diff --git a/app/models/instance_metadata/kas.rb b/app/models/instance_metadata/kas.rb
new file mode 100644
index 00000000000..7d2d71120b5
--- /dev/null
+++ b/app/models/instance_metadata/kas.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class InstanceMetadata::Kas
+ attr_reader :enabled, :version, :external_url
+
+ def initialize
+ @enabled = Gitlab::Kas.enabled?
+ @version = Gitlab::Kas.version if @enabled
+ @external_url = Gitlab::Kas.external_url if @enabled
+ end
+
+ def self.declarative_policy_class
+ "InstanceMetadataPolicy"
+ end
+end
diff --git a/app/models/service.rb b/app/models/integration.rb
index aadc75ae710..13203cd4e95 100644
--- a/app/models/service.rb
+++ b/app/models/integration.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-# To add new service you should build a class inherited from Service
+# To add new integration you should build a class inherited from Integration
# and implement a set of methods
-class Service < ApplicationRecord
+class Integration < ApplicationRecord
include Sortable
include Importable
include ProjectServicesLoggable
@@ -10,24 +10,29 @@ class Service < ApplicationRecord
include FromUnion
include EachBatch
- SERVICE_NAMES = %w[
+ # TODO Rename the table: https://gitlab.com/gitlab-org/gitlab/-/issues/201856
+ self.table_name = 'services'
+
+ INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
- PROJECT_SPECIFIC_SERVICE_NAMES = %w[
+ PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
datadog jenkins
].freeze
- # Fake services to help with local development.
- DEV_SERVICE_NAMES = %w[
+ # Fake integrations to help with local development.
+ DEV_INTEGRATION_NAMES = %w[
mock_ci mock_monitoring
].freeze
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :type, Gitlab::Integrations::StiType.new
+
default_value_for :active, false
default_value_for :alert_events, true
default_value_for :category, 'common'
@@ -47,18 +52,18 @@ class Service < ApplicationRecord
after_commit :reset_updated_properties
- belongs_to :project, inverse_of: :services
- belongs_to :group, inverse_of: :services
- has_one :service_hook
+ belongs_to :project, inverse_of: :integrations
+ belongs_to :group, inverse_of: :integrations
+ has_one :service_hook, inverse_of: :integration, foreign_key: :service_id
- validates :project_id, presence: true, unless: -> { template? || instance? || group_id }
- validates :group_id, presence: true, unless: -> { template? || instance? || project_id }
- validates :project_id, :group_id, absence: true, if: -> { template? || instance? }
+ validates :project_id, presence: true, unless: -> { template? || instance_level? || group_level? }
+ validates :group_id, presence: true, unless: -> { template? || instance_level? || project_level? }
+ validates :project_id, :group_id, absence: true, if: -> { template? || instance_level? }
validates :type, presence: true
validates :type, uniqueness: { scope: :template }, if: :template?
- validates :type, uniqueness: { scope: :instance }, if: :instance?
- validates :type, uniqueness: { scope: :project_id }, if: :project_id?
- validates :type, uniqueness: { scope: :group_id }, if: :group_id?
+ validates :type, uniqueness: { scope: :instance }, if: :instance_level?
+ validates :type, uniqueness: { scope: :project_id }, if: :project_level?
+ validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_is_instance_or_template
validate :validate_belongs_to_project_or_group
@@ -164,22 +169,23 @@ class Service < ApplicationRecord
end
def self.create_nonexistent_templates
- nonexistent_services = list_nonexistent_services_for(for_template)
+ nonexistent_services = build_nonexistent_services_for(for_template)
return if nonexistent_services.empty?
# Create within a transaction to perform the lowest possible SQL queries.
transaction do
- nonexistent_services.each do |service_type|
- service_type.constantize.create(template: true)
+ nonexistent_services.each do |service|
+ service.template = true
+ service.save
end
end
end
private_class_method :create_nonexistent_templates
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
- if name.in?(available_services_names(include_project_specific: false))
- "#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id)
- end
+ return unless name.in?(available_services_names(include_project_specific: false))
+
+ service_name_to_model(name).find_or_initialize_by(instance: instance, group_id: group_id)
end
def self.find_or_initialize_all_non_project_specific(scope)
@@ -187,19 +193,23 @@ class Service < ApplicationRecord
end
def self.build_nonexistent_services_for(scope)
- list_nonexistent_services_for(scope).map do |service_type|
- service_type.constantize.new
+ nonexistent_services_types_for(scope).map do |service_type|
+ service_type_to_model(service_type).new
end
end
private_class_method :build_nonexistent_services_for
- def self.list_nonexistent_services_for(scope)
+ # Returns a list of service types that do not exist in the given scope.
+ # Example: ["AsanaService", ...]
+ def self.nonexistent_services_types_for(scope)
# Using #map instead of #pluck to save one query count. This is because
# ActiveRecord loaded the object here, so we don't need to query again later.
available_services_types(include_project_specific: false) - scope.map(&:type)
end
- private_class_method :list_nonexistent_services_for
+ private_class_method :nonexistent_services_types_for
+ # Returns a list of available service names.
+ # Example: ["asana", ...]
def self.available_services_names(include_project_specific: true, include_dev: true)
service_names = services_names
service_names += project_specific_services_names if include_project_specific
@@ -209,40 +219,61 @@ class Service < ApplicationRecord
end
def self.services_names
- SERVICE_NAMES
+ INTEGRATION_NAMES
end
def self.dev_services_names
return [] unless Rails.env.development?
- DEV_SERVICE_NAMES
+ DEV_INTEGRATION_NAMES
end
def self.project_specific_services_names
- PROJECT_SPECIFIC_SERVICE_NAMES
+ PROJECT_SPECIFIC_INTEGRATION_NAMES
end
+ # Returns a list of available service types.
+ # Example: ["AsanaService", ...]
def self.available_services_types(include_project_specific: true, include_dev: true)
available_services_names(include_project_specific: include_project_specific, include_dev: include_dev).map do |service_name|
- "#{service_name}_service".camelize
+ service_name_to_type(service_name)
end
end
+ # Returns the model for the given service name.
+ # Example: "asana" => Integrations::Asana
+ def self.service_name_to_model(name)
+ type = service_name_to_type(name)
+ service_type_to_model(type)
+ end
+
+ # Returns the STI type for the given service name.
+ # Example: "asana" => "AsanaService"
+ def self.service_name_to_type(name)
+ "#{name}_service".camelize
+ end
+
+ # Returns the model for the given STI type.
+ # Example: "AsanaService" => Integrations::Asana
+ def self.service_type_to_model(type)
+ Gitlab::Integrations::StiType.new.cast(type).constantize
+ end
+ private_class_method :service_type_to_model
+
def self.build_from_integration(integration, project_id: nil, group_id: nil)
- service = integration.dup
+ new_integration = integration.dup
if integration.supports_data_fields?
data_fields = integration.data_fields.dup
- data_fields.service = service
+ data_fields.integration = new_integration
end
- service.template = false
- service.instance = false
- service.project_id = project_id
- service.group_id = group_id
- service.inherit_from_id = integration.id if integration.instance? || integration.group
- service.active = false if service.invalid?
- service
+ new_integration.template = false
+ new_integration.instance = false
+ new_integration.project_id = project_id
+ new_integration.group_id = group_id
+ new_integration.inherit_from_id = integration.id if integration.instance_level? || integration.group_level?
+ new_integration
end
def self.instance_exists_for?(type)
@@ -269,7 +300,7 @@ class Service < ApplicationRecord
private_class_method :instance_level_integration
def self.create_from_active_default_integrations(scope, association, with_templates: false)
- group_ids = scope.ancestors.select(:id)
+ group_ids = sorted_ancestors(scope).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
from_union([
@@ -340,7 +371,7 @@ class Service < ApplicationRecord
# Expose a list of fields in the JSON endpoint.
#
- # This list is used in `Service#as_json(only: json_fields)`.
+ # This list is used in `Integration#as_json(only: json_fields)`.
def json_fields
%w[active]
end
@@ -407,16 +438,24 @@ class Service < ApplicationRecord
{ success: result.present?, result: result }
end
- # Disable test for instance-level and group-level services.
+ # Disable test for instance-level and group-level integrations.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
- !instance? && !group_id
+ !(instance_level? || group_level?)
end
def project_level?
project_id.present?
end
+ def group_level?
+ group_id.present?
+ end
+
+ def instance_level?
+ instance?
+ end
+
def parent
project || group
end
@@ -424,7 +463,7 @@ class Service < ApplicationRecord
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
# ActiveRecord does not provide a mechanism to track changes in serialized keys,
- # so we need a specific implementation for service properties.
+ # so we need a specific implementation for integration properties.
# This allows to track changes to properties set with the accessor methods,
# but not direct manipulation of properties hash.
def updated_properties
@@ -452,12 +491,21 @@ class Service < ApplicationRecord
private
+ # Ancestors sorted by hierarchy depth in bottom-top order.
+ def self.sorted_ancestors(scope)
+ if scope.root_ancestor.use_traversal_ids?
+ Namespace.from(scope.ancestors(hierarchy_order: :asc))
+ else
+ scope.ancestors
+ end
+ end
+
def validate_is_instance_or_template
- errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
+ errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level?
end
def validate_belongs_to_project_or_group
- errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id
+ errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level?
end
def validate_recipients?
@@ -465,4 +513,4 @@ class Service < ApplicationRecord
end
end
-Service.prepend_if_ee('EE::Service')
+Integration.prepend_mod_with('Integration')
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
new file mode 100644
index 00000000000..7949563a1dc
--- /dev/null
+++ b/app/models/integrations/asana.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'asana'
+
+module Integrations
+ class Asana < Integration
+ include ActionView::Helpers::UrlHelper
+
+ prop_accessor :api_key, :restrict_to_branch
+ validates :api_key, presence: true, if: :activated?
+
+ def title
+ 'Asana'
+ end
+
+ def description
+ s_('AsanaService|Add commit messages as comments to Asana tasks.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'asana'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'api_key',
+ title: 'API key',
+ help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
+ # Example Personal Access Token from Asana docs
+ placeholder: '0/68a9e79b868c6789e79a124c30b0',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ title: 'Restrict to branch (optional)',
+ help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
+ }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def client
+ @_client ||= begin
+ ::Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ # check the branch restriction is poplulated and branch is not included
+ branch = Gitlab::Git.ref_name(data[:ref])
+ branch_restriction = restrict_to_branch.to_s
+ if branch_restriction.present? && branch_restriction.index(branch).nil?
+ return
+ end
+
+ user = data[:user_name]
+ project_name = project.full_name
+
+ data[:commits].each do |commit|
+ push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
+ check_commit(commit[:message], push_msg)
+ end
+ end
+
+ def check_commit(message, push_msg)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/{project_gid}/{task_gid}
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = ::Asana::Resources::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue StandardError => e
+ log_error(e.message)
+ next
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
new file mode 100644
index 00000000000..6a36045330a
--- /dev/null
+++ b/app/models/integrations/assembla.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Assembla < Integration
+ prop_accessor :token, :subdomain
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'Assembla'
+ end
+
+ def description
+ _('Manage projects.')
+ end
+
+ def self.to_param
+ 'assembla'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'subdomain', placeholder: '' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
+ Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
+ end
+ end
+end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
new file mode 100644
index 00000000000..82111c7322e
--- /dev/null
+++ b/app/models/integrations/bamboo.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Bamboo < CiService
+ include ActionView::Helpers::UrlHelper
+ include ReactiveService
+
+ prop_accessor :bamboo_url, :build_key, :username, :password
+
+ validates :bamboo_url, presence: true, public_url: true, if: :activated?
+ validates :build_key, presence: true, if: :activated?
+ validates :username,
+ presence: true,
+ if: ->(service) { service.activated? && service.password }
+ validates :password,
+ presence: true,
+ if: ->(service) { service.activated? && service.username }
+
+ attr_accessor :response
+
+ after_save :compose_service_hook, if: :activated?
+ before_update :reset_password
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.save
+ end
+
+ def reset_password
+ if bamboo_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
+
+ def title
+ s_('BambooService|Atlassian Bamboo')
+ end
+
+ def description
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'bamboo'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'bamboo_url',
+ title: s_('BambooService|Bamboo URL'),
+ placeholder: s_('https://bamboo.example.com'),
+ help: s_('BambooService|Bamboo service root URL.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_key',
+ placeholder: s_('KEY'),
+ help: s_('BambooService|Bamboo build plan key.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ help: s_('BambooService|The user with API access to the Bamboo server.')
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
+ ]
+ end
+
+ def build_page(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
+ end
+
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ get_path("updateAndBuild.action", { buildKey: build_key })
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
+
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ end
+
+ private
+
+ def get_build_result(response)
+ return if response&.code != 200
+
+ # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
+ result = response.dig('results', 'results', 'result')
+
+ # In case of multiple results, arbitrarily assume the last one is the most relevant.
+ return result.last if result.is_a?(Array)
+
+ result
+ end
+
+ def read_build_page(response)
+ result = get_build_result(response)
+ key =
+ if result.blank?
+ # If actual build link can't be determined, send user to build summary page.
+ build_key
+ else
+ # If actual build link is available, go to build result page.
+ result.dig('planResultKey', 'key')
+ end
+
+ build_url("browse/#{key}")
+ end
+
+ def read_commit_status(response)
+ return :error unless response && (response.code == 200 || response.code == 404)
+
+ result = get_build_result(response)
+ status =
+ if result.blank?
+ 'Pending'
+ else
+ result.dig('buildState')
+ end
+
+ return :error unless status.present?
+
+ if status.include?('Success')
+ 'success'
+ elsif status.include?('Failed')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
+ def try_get_path(path, query_params = {})
+ params = build_get_params(query_params)
+ params[:extra_log_info] = { project_id: project_id }
+
+ Gitlab::HTTP.try_get(build_url(path), params)
+ end
+
+ def get_path(path, query_params = {})
+ Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
+ end
+
+ def build_url(path)
+ Gitlab::Utils.append_path(bamboo_url, path)
+ end
+
+ def build_get_params(query_params)
+ params = { verify: false, query: query_params }
+ return params if username.blank? && password.blank?
+
+ query_params[:os_authType] = 'basic'
+ params[:basic_auth] = basic_auth
+ params
+ end
+
+ def basic_auth
+ { username: username, password: password }
+ end
+ end
+end
diff --git a/app/models/integrations/builds_email.rb b/app/models/integrations/builds_email.rb
new file mode 100644
index 00000000000..2628848667e
--- /dev/null
+++ b/app/models/integrations/builds_email.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
+# https://gitlab.com/gitlab-org/gitlab/-/issues/331064
+module Integrations
+ class BuildsEmail < Integration
+ def self.to_param
+ 'builds_email'
+ end
+
+ def self.supported_events
+ %w[]
+ end
+ end
+end
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
new file mode 100644
index 00000000000..eede3d00307
--- /dev/null
+++ b/app/models/integrations/campfire.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Campfire < Integration
+ prop_accessor :token, :subdomain, :room
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'Campfire'
+ end
+
+ def description
+ 'Send notifications about push events to Campfire chat rooms.'
+ end
+
+ def self.to_param
+ 'campfire'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'subdomain', placeholder: '' },
+ { type: 'text', name: 'room', placeholder: '' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ message = build_message(data)
+ speak(self.room, message, auth)
+ end
+
+ private
+
+ def base_uri
+ @base_uri ||= "https://#{subdomain}.campfirenow.com"
+ end
+
+ def auth
+ # use a dummy password, as explained in the Campfire API doc:
+ # https://github.com/basecamp/campfire-api#authentication
+ @auth ||= {
+ basic_auth: {
+ username: token,
+ password: 'X'
+ }
+ }
+ end
+
+ # Post a message into a room, returns the message Hash in case of success.
+ # Returns nil otherwise.
+ # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
+ def speak(room_name, message, auth)
+ room = rooms(auth).find { |r| r["name"] == room_name }
+ return unless room
+
+ path = "/room/#{room["id"]}/speak.json"
+ body = {
+ body: {
+ message: {
+ type: 'TextMessage',
+ body: message
+ }
+ }
+ }
+ res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body))
+ res.code == 201 ? res : nil
+ end
+
+ # Returns a list of rooms, or [].
+ # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
+ def rooms(auth)
+ res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth)
+ res.code == 200 ? res["rooms"] : []
+ end
+
+ def build_message(push)
+ ref = Gitlab::Git.ref_name(push[:ref])
+ before = push[:before]
+ after = push[:after]
+
+ message = []
+ message << "[#{project.full_name}] "
+ message << "#{push[:user_name]} "
+
+ if Gitlab::Git.blank_ref?(before)
+ message << "pushed new branch #{ref} \n"
+ elsif Gitlab::Git.blank_ref?(after)
+ message << "removed branch #{ref} \n"
+ else
+ message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
+ message << "#{project.web_url}/compare/#{before}...#{after}"
+ end
+
+ message.join
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb
new file mode 100644
index 00000000000..ef0579124fe
--- /dev/null
+++ b/app/models/integrations/chat_message/alert_message.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class AlertMessage < BaseMessage
+ attr_reader :title
+ attr_reader :alert_url
+ attr_reader :severity
+ attr_reader :events
+ attr_reader :status
+ attr_reader :started_at
+
+ def initialize(params)
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @title = params.dig(:object_attributes, :title)
+ @alert_url = params.dig(:object_attributes, :url)
+ @severity = params.dig(:object_attributes, :severity)
+ @events = params.dig(:object_attributes, :events)
+ @status = params.dig(:object_attributes, :status)
+ @started_at = params.dig(:object_attributes, :started_at)
+ end
+
+ def attachments
+ [{
+ title: title,
+ title_link: alert_url,
+ color: attachment_color,
+ fields: attachment_fields
+ }]
+ end
+
+ def message
+ "Alert firing in #{project_name}"
+ end
+
+ private
+
+ def attachment_color
+ "#C95823"
+ end
+
+ def attachment_fields
+ [
+ {
+ title: "Severity",
+ value: severity.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Events",
+ value: events,
+ short: true
+ },
+ {
+ title: "Status",
+ value: status.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Start time",
+ value: format_time(started_at),
+ short: true
+ }
+ ]
+ end
+
+ # This formats time into the following format
+ # April 23rd, 2020 1:06AM UTC
+ def format_time(time)
+ time = Time.zone.parse(time.to_s)
+ time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb
new file mode 100644
index 00000000000..2f70384d3b9
--- /dev/null
+++ b/app/models/integrations/chat_message/base_message.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class BaseMessage
+ RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze
+
+ attr_reader :markdown
+ attr_reader :user_full_name
+ attr_reader :user_name
+ attr_reader :user_avatar
+ attr_reader :project_name
+ attr_reader :project_url
+
+ def initialize(params)
+ @markdown = params[:markdown] || false
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @user_full_name = params.dig(:user, :name) || params[:user_full_name]
+ @user_name = params.dig(:user, :username) || params[:user_name]
+ @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
+ end
+
+ def user_combined_name
+ if user_full_name.present?
+ "#{user_full_name} (#{user_name})"
+ else
+ user_name
+ end
+ end
+
+ def summary
+ return message if markdown
+
+ format(message)
+ end
+
+ def pretext
+ summary
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ raise NotImplementedError
+ end
+
+ def activity
+ raise NotImplementedError
+ end
+
+ private
+
+ def message
+ raise NotImplementedError
+ end
+
+ def format(string)
+ Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
+ end
+
+ def format_relative_links(string)
+ string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
+ end
+
+ def attachment_color
+ '#345'
+ end
+
+ def link(text, url)
+ "[#{text}](#{url})"
+ end
+
+ def pretty_duration(seconds)
+ parse_string =
+ if duration < 1.hour
+ '%M:%S'
+ else
+ '%H:%M:%S'
+ end
+
+ Time.at(seconds).utc.strftime(parse_string)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb
new file mode 100644
index 00000000000..c4f3bf9610d
--- /dev/null
+++ b/app/models/integrations/chat_message/deployment_message.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class DeploymentMessage < BaseMessage
+ attr_reader :commit_title
+ attr_reader :commit_url
+ attr_reader :deployable_id
+ attr_reader :deployable_url
+ attr_reader :environment
+ attr_reader :short_sha
+ attr_reader :status
+ attr_reader :user_url
+
+ def initialize(data)
+ super
+
+ @commit_title = data[:commit_title]
+ @commit_url = data[:commit_url]
+ @deployable_id = data[:deployable_id]
+ @deployable_url = data[:deployable_url]
+ @environment = data[:environment]
+ @short_sha = data[:short_sha]
+ @status = data[:status]
+ @user_url = data[:user_url]
+ end
+
+ def attachments
+ [{
+ text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
+ color: color
+ }]
+ end
+
+ def activity
+ {}
+ end
+
+ private
+
+ def message
+ if running?
+ "Starting deploy to #{environment}"
+ else
+ "Deploy to #{environment} #{humanized_status}"
+ end
+ end
+
+ def color
+ case status
+ when 'success'
+ 'good'
+ when 'canceled'
+ 'warning'
+ when 'failed'
+ 'danger'
+ else
+ '#334455'
+ end
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def deployment_link
+ link("##{deployable_id}", deployable_url)
+ end
+
+ def user_link
+ link(user_combined_name, user_url)
+ end
+
+ def commit_link
+ link(short_sha, commit_url)
+ end
+
+ def humanized_status
+ status == 'success' ? 'succeeded' : status
+ end
+
+ def running?
+ status == 'running'
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb
new file mode 100644
index 00000000000..5fa6bd4090f
--- /dev/null
+++ b/app/models/integrations/chat_message/issue_message.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class IssueMessage < BaseMessage
+ attr_reader :title
+ attr_reader :issue_iid
+ attr_reader :issue_url
+ attr_reader :action
+ attr_reader :state
+ attr_reader :description
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @issue_iid = obj_attr[:iid]
+ @issue_url = obj_attr[:url]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @description = obj_attr[:description] || ''
+ end
+
+ def attachments
+ return [] unless opened_issue?
+ return description if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "Issue #{state} by #{user_combined_name}",
+ subtitle: "in #{project_link}",
+ text: issue_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
+ end
+
+ def opened_issue?
+ action == 'open'
+ end
+
+ def description_message
+ [{
+ title: issue_title,
+ title_link: issue_url,
+ text: format(description),
+ color: '#C95823'
+ }]
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def issue_link
+ link(issue_title, issue_url)
+ end
+
+ def issue_title
+ "#{Issue.reference_prefix}#{issue_iid} #{title}"
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/merge_message.rb b/app/models/integrations/chat_message/merge_message.rb
new file mode 100644
index 00000000000..d2f48699f50
--- /dev/null
+++ b/app/models/integrations/chat_message/merge_message.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class MergeMessage < BaseMessage
+ attr_reader :merge_request_iid
+ attr_reader :source_branch
+ attr_reader :target_branch
+ attr_reader :action
+ attr_reader :state
+ attr_reader :title
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @merge_request_iid = obj_attr[:iid]
+ @source_branch = obj_attr[:source_branch]
+ @target_branch = obj_attr[:target_branch]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @title = format_title(obj_attr[:title])
+ end
+
+ def attachments
+ []
+ end
+
+ def activity
+ {
+ title: "Merge request #{state_or_action_text} by #{user_combined_name}",
+ subtitle: "in #{project_link}",
+ text: merge_request_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def format_title(title)
+ '*' + title.lines.first.chomp + '*'
+ end
+
+ def message
+ merge_request_message
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def merge_request_message
+ "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
+ end
+
+ def merge_request_link
+ link(merge_request_title, merge_request_url)
+ end
+
+ def merge_request_title
+ "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
+ end
+
+ def merge_request_url
+ "#{project_url}/-/merge_requests/#{merge_request_iid}"
+ end
+
+ def state_or_action_text
+ case action
+ when 'approved', 'unapproved'
+ action
+ when 'approval'
+ 'added their approval to'
+ when 'unapproval'
+ 'removed their approval from'
+ else
+ state
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/note_message.rb b/app/models/integrations/chat_message/note_message.rb
new file mode 100644
index 00000000000..96675d2b27c
--- /dev/null
+++ b/app/models/integrations/chat_message/note_message.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class NoteMessage < BaseMessage
+ attr_reader :note
+ attr_reader :note_url
+ attr_reader :title
+ attr_reader :target
+
+ def initialize(params)
+ super
+
+ params = HashWithIndifferentAccess.new(params)
+ obj_attr = params[:object_attributes]
+ @note = obj_attr[:note]
+ @note_url = obj_attr[:url]
+ @target, @title = case obj_attr[:noteable_type]
+ when "Commit"
+ create_commit_note(params[:commit])
+ when "Issue"
+ create_issue_note(params[:issue])
+ when "MergeRequest"
+ create_merge_note(params[:merge_request])
+ when "Snippet"
+ create_snippet_note(params[:snippet])
+ end
+ end
+
+ def attachments
+ return note if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
+ subtitle: "in #{project_link}",
+ text: formatted_title,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
+ end
+
+ def format_title(title)
+ title.lines.first.chomp
+ end
+
+ def formatted_title
+ format_title(title)
+ end
+
+ def create_issue_note(issue)
+ ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
+ end
+
+ def create_commit_note(commit)
+ commit_sha = Commit.truncate_sha(commit[:id])
+
+ ["commit #{commit_sha}", commit[:message]]
+ end
+
+ def create_merge_note(merge_request)
+ ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
+ end
+
+ def create_snippet_note(snippet)
+ ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
+ end
+
+ def description_message
+ [{ text: format(note), color: attachment_color }]
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
new file mode 100644
index 00000000000..a0f6f582e4c
--- /dev/null
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -0,0 +1,267 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class PipelineMessage < BaseMessage
+ MAX_VISIBLE_JOBS = 10
+
+ attr_reader :user
+ attr_reader :ref_type
+ attr_reader :ref
+ attr_reader :status
+ attr_reader :detailed_status
+ attr_reader :duration
+ attr_reader :finished_at
+ attr_reader :pipeline_id
+ attr_reader :failed_stages
+ attr_reader :failed_jobs
+
+ attr_reader :project
+ attr_reader :commit
+ attr_reader :committer
+ attr_reader :pipeline
+
+ def initialize(data)
+ super
+
+ @user = data[:user]
+ @user_name = data.dig(:user, :username) || 'API'
+
+ pipeline_attributes = data[:object_attributes]
+ @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ @ref = pipeline_attributes[:ref]
+ @status = pipeline_attributes[:status]
+ @detailed_status = pipeline_attributes[:detailed_status]
+ @duration = pipeline_attributes[:duration].to_i
+ @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
+ @pipeline_id = pipeline_attributes[:id]
+
+ # Get list of jobs that have actually failed (after exhausting all retries)
+ @failed_jobs = actually_failed_jobs(Array(data[:builds]))
+ @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
+
+ @project = Project.find(data[:project][:id])
+ @commit = project.commit_by(oid: data[:commit][:id])
+ @committer = commit.committer
+ @pipeline = Ci::Pipeline.find(pipeline_id)
+ end
+
+ def pretext
+ ''
+ end
+
+ def attachments
+ return message if markdown
+
+ [{
+ fallback: format(message),
+ color: attachment_color,
+ author_name: user_combined_name,
+ author_icon: user_avatar,
+ author_link: author_url,
+ title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
+ {
+ pipeline_id: pipeline_id,
+ humanized_status: humanized_status,
+ duration: pretty_duration(duration)
+ },
+ title_link: pipeline_url,
+ fields: attachments_fields,
+ footer: project.name,
+ footer_icon: project.avatar_url(only_path: false),
+ ts: finished_at
+ }]
+ end
+
+ def activity
+ {
+ title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
+ {
+ pipeline_link: pipeline_link,
+ ref_type: ref_type,
+ ref_link: ref_link,
+ user_combined_name: user_combined_name,
+ humanized_status: humanized_status
+ },
+ subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
+ text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
+ image: user_avatar || ''
+ }
+ end
+
+ private
+
+ def actually_failed_jobs(builds)
+ succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
+
+ failed_jobs = builds.select do |build|
+ # Select jobs which doesn't have a successful retry
+ build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
+ end
+
+ failed_jobs.uniq { |job| job[:name] }.reverse
+ end
+
+ def failed_stages_field
+ {
+ title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
+ value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
+ short: true
+ }
+ end
+
+ def failed_jobs_field
+ {
+ title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
+ value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
+ short: true
+ }
+ end
+
+ def yaml_error_field
+ {
+ title: s_("ChatMessage|Invalid CI config YAML file"),
+ value: pipeline.yaml_errors,
+ short: false
+ }
+ end
+
+ def attachments_fields
+ fields = [
+ {
+ title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
+ value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
+ short: true
+ },
+ {
+ title: s_("ChatMessage|Commit"),
+ value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
+ short: true
+ }
+ ]
+
+ fields << failed_stages_field if failed_stages.any?
+ fields << failed_jobs_field if failed_jobs.any?
+ fields << yaml_error_field if pipeline.has_yaml_errors?
+
+ fields
+ end
+
+ def message
+ s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
+ {
+ project_link: project_link,
+ pipeline_link: pipeline_link,
+ ref_type: ref_type,
+ ref_link: ref_link,
+ user_combined_name: user_combined_name,
+ humanized_status: humanized_status,
+ duration: pretty_duration(duration)
+ }
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
+ when 'failed'
+ s_("ChatMessage|has failed")
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/-/commits/#{ref}"
+ end
+ end
+
+ def ref_link
+ "[#{ref}](#{ref_url})"
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def project_link
+ "[#{project.name}](#{project_url})"
+ end
+
+ def pipeline_failed_jobs_url
+ "#{project_url}/-/pipelines/#{pipeline_id}/failures"
+ end
+
+ def pipeline_url
+ if failed_jobs.any?
+ pipeline_failed_jobs_url
+ else
+ "#{project_url}/-/pipelines/#{pipeline_id}"
+ end
+ end
+
+ def pipeline_link
+ "[##{pipeline_id}](#{pipeline_url})"
+ end
+
+ def job_url(job)
+ "#{project_url}/-/jobs/#{job[:id]}"
+ end
+
+ def job_link(job)
+ "[#{job[:name]}](#{job_url(job)})"
+ end
+
+ def failed_jobs_links
+ failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
+ truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
+
+ failed_links = failed.map { |job| job_link(job) }
+
+ unless truncated.blank?
+ failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
+ count: truncated.size,
+ pipeline_failed_jobs_url: pipeline_failed_jobs_url
+ }
+ end
+
+ failed_links.join(I18n.t(:'support.array.words_connector'))
+ end
+
+ def stage_link(stage)
+ # All stages link to the pipeline page
+ "[#{stage}](#{pipeline_url})"
+ end
+
+ def failed_stages_links
+ failed_stages.map { |s| stage_link(s) }.join(I18n.t(:'support.array.words_connector'))
+ end
+
+ def commit_url
+ Gitlab::UrlBuilder.build(commit)
+ end
+
+ def commit_link
+ "[#{commit.title}](#{commit_url})"
+ end
+
+ def author_url
+ return unless user && committer
+
+ Gitlab::UrlBuilder.build(committer)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
new file mode 100644
index 00000000000..0952986e923
--- /dev/null
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class PushMessage < BaseMessage
+ attr_reader :after
+ attr_reader :before
+ attr_reader :commits
+ attr_reader :ref
+ attr_reader :ref_type
+
+ def initialize(params)
+ super
+
+ @after = params[:after]
+ @before = params[:before]
+ @commits = params.fetch(:commits, [])
+ @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
+ @ref = Gitlab::Git.ref_name(params[:ref])
+ end
+
+ def attachments
+ return [] if new_branch? || removed_branch?
+ return commit_messages if markdown
+
+ commit_message_attachments
+ end
+
+ def activity
+ {
+ title: humanized_action(short: true),
+ subtitle: "in #{project_link}",
+ text: compare_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def humanized_action(short: false)
+ action, ref_link, target_link = compose_action_details
+ text = [user_combined_name, action, ref_type, ref_link]
+ text << target_link unless short
+ text.join(' ')
+ end
+
+ def message
+ humanized_action
+ end
+
+ def format(string)
+ Slack::Messenger::Util::LinkFormatter.format(string)
+ end
+
+ def commit_messages
+ commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
+ end
+
+ def commit_message_attachments
+ [{ text: format(commit_messages), color: attachment_color }]
+ end
+
+ def compose_commit_message(commit)
+ author = commit[:author][:name]
+ id = Commit.truncate_sha(commit[:id])
+ title = commit[:title]
+
+ url = commit[:url]
+
+ "[#{id}](#{url}): #{title} - #{author}"
+ end
+
+ def new_branch?
+ Gitlab::Git.blank_ref?(before)
+ end
+
+ def removed_branch?
+ Gitlab::Git.blank_ref?(after)
+ end
+
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
+ end
+
+ def compare_url
+ "#{project_url}/compare/#{before}...#{after}"
+ end
+
+ def ref_link
+ "[#{ref}](#{ref_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def compare_link
+ "[Compare changes](#{compare_url})"
+ end
+
+ def compose_action_details
+ if new_branch?
+ ['pushed new', ref_link, "to #{project_link}"]
+ elsif removed_branch?
+ ['removed', ref, "from #{project_link}"]
+ else
+ ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
+ end
+ end
+
+ def attachment_color
+ '#345'
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/wiki_page_message.rb b/app/models/integrations/chat_message/wiki_page_message.rb
new file mode 100644
index 00000000000..9b5275b8c03
--- /dev/null
+++ b/app/models/integrations/chat_message/wiki_page_message.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class WikiPageMessage < BaseMessage
+ attr_reader :title
+ attr_reader :wiki_page_url
+ attr_reader :action
+ attr_reader :description
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @wiki_page_url = obj_attr[:url]
+ @description = obj_attr[:message]
+
+ @action =
+ case obj_attr[:action]
+ when "create"
+ "created"
+ when "update"
+ "edited"
+ end
+ end
+
+ def attachments
+ return description if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "#{user_combined_name} #{action} #{wiki_page_link}",
+ subtitle: "in #{project_link}",
+ text: title,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ end
+
+ def description_message
+ [{ text: format(@description), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def wiki_page_link
+ "[wiki page](#{wiki_page_url})"
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
new file mode 100644
index 00000000000..30f73496993
--- /dev/null
+++ b/app/models/integrations/confluence.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Confluence < Integration
+ include ActionView::Helpers::UrlHelper
+
+ VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze
+ VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
+ VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
+
+ prop_accessor :confluence_url
+
+ validates :confluence_url, presence: true, if: :activated?
+ validate :validate_confluence_url_is_cloud, if: :activated?
+
+ after_commit :cache_project_has_confluence
+
+ def self.to_param
+ 'confluence'
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def title
+ s_('ConfluenceService|Confluence Workspace')
+ end
+
+ def description
+ s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.')
+ end
+
+ def help
+ return unless project&.wiki_enabled?
+
+ if activated?
+ wiki_url = project.wiki.web_url
+
+ s_(
+ 'ConfluenceService|Your GitLab wiki is still available at %{wiki_link}. To re-enable the link to the GitLab wiki, disable this integration.' %
+ { wiki_link: link_to(wiki_url, wiki_url) }
+ ).html_safe
+ else
+ s_('ConfluenceService|Link to a Confluence Workspace from the sidebar. Enabling this integration replaces the "Wiki" sidebar link with a link to the Confluence Workspace. The GitLab wiki is still available at the original URL.').html_safe
+ end
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'confluence_url',
+ title: s_('Confluence Cloud Workspace URL'),
+ placeholder: 'https://example.atlassian.net/wiki',
+ required: true
+ }
+ ]
+ end
+
+ def can_test?
+ false
+ end
+
+ private
+
+ def validate_confluence_url_is_cloud
+ unless confluence_uri_valid?
+ errors.add(:confluence_url, 'URL must be to a Confluence Cloud Workspace hosted on atlassian.net')
+ end
+ end
+
+ def confluence_uri_valid?
+ return false unless confluence_url
+
+ uri = URI.parse(confluence_url)
+
+ (uri.scheme&.match(VALID_SCHEME_MATCH) &&
+ uri.host&.match(VALID_HOST_MATCH) &&
+ uri.path&.match(VALID_PATH_MATCH)).present?
+
+ rescue URI::InvalidURIError
+ false
+ end
+
+ def cache_project_has_confluence
+ return unless project && !project.destroyed?
+
+ project.project_setting.save! unless project.project_setting.persisted?
+ project.project_setting.update_column(:has_confluence, active?)
+ end
+ end
+end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
new file mode 100644
index 00000000000..dd4b0664d52
--- /dev/null
+++ b/app/models/integrations/datadog.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Datadog < Integration
+ DEFAULT_SITE = 'datadoghq.com'
+ URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'
+ URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'
+ URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/"
+
+ SUPPORTED_EVENTS = %w[
+ pipeline job
+ ].freeze
+
+ prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env
+
+ with_options if: :activated? do
+ validates :api_key, presence: true, format: { with: /\A\w+\z/ }
+ validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
+ validates :api_url, public_url: { allow_blank: true }
+ validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
+ validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
+ end
+
+ after_save :compose_service_hook, if: :activated?
+
+ def initialize_properties
+ super
+
+ self.datadog_site ||= DEFAULT_SITE
+ end
+
+ def self.supported_events
+ SUPPORTED_EVENTS
+ end
+
+ def self.default_test_event
+ 'pipeline'
+ end
+
+ def configurable_events
+ [] # do not allow to opt out of required hooks
+ end
+
+ def title
+ 'Datadog'
+ end
+
+ def description
+ 'Trace your GitLab pipelines with Datadog'
+ end
+
+ def help
+ nil
+ end
+
+ def self.to_param
+ 'datadog'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'datadog_site',
+ placeholder: DEFAULT_SITE,
+ help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
+ required: false
+ },
+ {
+ type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ help: '(Advanced) Define the full URL for your Datadog site directly',
+ required: false
+ },
+ {
+ type: 'password',
+ name: 'api_key',
+ title: _('API key'),
+ non_empty_password_title: s_('ProjectService|Enter new API key'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
+ help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'datadog_service',
+ title: 'Service',
+ placeholder: 'gitlab-ci',
+ help: 'Name of this GitLab instance that all data will be tagged with'
+ },
+ {
+ type: 'text',
+ name: 'datadog_env',
+ title: 'Env',
+ help: 'The environment tag that traces will be tagged with'
+ }
+ ]
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def hook_url
+ url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site)
+ url = URI.parse(url)
+ url.path = File.join(url.path || '/', api_key)
+ query = { service: datadog_service.presence, env: datadog_env.presence }.compact
+ url.query = query.to_query unless query.empty?
+ url.to_s
+ end
+
+ def api_keys_url
+ return URL_API_KEYS_DOCS unless datadog_site.presence
+
+ sprintf(URL_TEMPLATE_API_KEYS, datadog_site: datadog_site)
+ end
+
+ def execute(data)
+ return if project.disabled_services.include?(to_param)
+
+ object_kind = data[:object_kind]
+ object_kind = 'job' if object_kind == 'build'
+ return unless supported_events.include?(object_kind)
+
+ service_hook.execute(data, "#{object_kind} hook")
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ return { success: false, result: result[:message] } if result[:http_status] != 200
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result[:message] }
+ end
+ end
+end
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
new file mode 100644
index 00000000000..e277633664f
--- /dev/null
+++ b/app/models/integrations/emails_on_push.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Integrations
+ class EmailsOnPush < Integration
+ include NotificationBranchSelection
+
+ RECIPIENTS_LIMIT = 750
+
+ boolean_accessor :send_from_committer_email
+ boolean_accessor :disable_diffs
+ prop_accessor :recipients, :branches_to_be_notified
+ validates :recipients, presence: true, if: :validate_recipients?
+ validate :number_of_recipients_within_limit, if: :validate_recipients?
+
+ def self.valid_recipients(recipients)
+ recipients.split.select do |recipient|
+ recipient.include?('@')
+ end.uniq(&:downcase)
+ end
+
+ def title
+ s_('EmailsOnPushService|Emails on push')
+ end
+
+ def description
+ s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
+ end
+
+ def self.to_param
+ 'emails_on_push'
+ end
+
+ def self.supported_events
+ %w(push tag_push)
+ end
+
+ def initialize_properties
+ super
+
+ self.branches_to_be_notified = 'all' if branches_to_be_notified.nil?
+ end
+
+ def execute(push_data)
+ return unless supported_events.include?(push_data[:object_kind])
+ return if project.emails_disabled?
+ return unless notify_for_ref?(push_data)
+
+ EmailsOnPushWorker.perform_async(
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
+ )
+ end
+
+ def notify_for_ref?(push_data)
+ return true if push_data[:object_kind] == 'tag_push'
+ return true if push_data.dig(:object_attributes, :tag)
+
+ notify_for_branch?(push_data)
+ end
+
+ def send_from_committer_email?
+ Gitlab::Utils.to_boolean(self.send_from_committer_email)
+ end
+
+ def disable_diffs?
+ Gitlab::Utils.to_boolean(self.disable_diffs)
+ end
+
+ def fields
+ domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
+ [
+ { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"),
+ help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
+ { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
+ help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
+ {
+ type: 'textarea',
+ name: 'recipients',
+ placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'),
+ help: s_('EmailsOnPushService|Emails separated by whitespace.')
+ }
+ ]
+ end
+
+ private
+
+ def number_of_recipients_within_limit
+ return if recipients.blank?
+
+ if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
+ errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ end
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index af78466e6a9..2077f9bfdbb 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -87,7 +87,8 @@ class Issue < ApplicationRecord
enum issue_type: {
issue: 0,
incident: 1,
- test_case: 2 ## EE-only
+ test_case: 2, ## EE-only
+ requirement: 3 ## EE-only
}
alias_method :issuing_parent, :project
@@ -108,6 +109,7 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
+ scope :order_relative_position_desc, -> { reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
@@ -121,7 +123,7 @@ class Issue < ApplicationRecord
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
scope :with_api_entity_associations, -> {
- preload(:timelogs, :closed_by, :assignees, :author, :notes, :labels,
+ preload(:timelogs, :closed_by, :assignees, :author, :labels,
milestone: { project: [:route, { namespace: :route }] },
project: [:route, { namespace: :route }])
}
@@ -174,8 +176,16 @@ class Issue < ApplicationRecord
state :opened, value: Issue.available_states[:opened]
state :closed, value: Issue.available_states[:closed]
- before_transition any => :closed do |issue|
+ before_transition any => :closed do |issue, transition|
+ args = transition.args
+
issue.closed_at = issue.system_note_timestamp
+
+ next if args.empty?
+
+ next unless args.first.is_a?(User)
+
+ issue.closed_by = args.first
end
before_transition closed: :opened do |issue|
@@ -262,6 +272,18 @@ class Issue < ApplicationRecord
"id DESC")
end
+ # Temporary disable moving null elements because of performance problems
+ # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
+ def check_repositioning_allowed!
+ if blocked_for_repositioning?
+ raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
+ end
+ end
+
+ def blocked_for_repositioning?
+ resource_parent.root_namespace&.issue_repositioning_disabled?
+ end
+
def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
@@ -506,4 +528,4 @@ class Issue < ApplicationRecord
end
end
-Issue.prepend_if_ee('EE::Issue')
+Issue.prepend_mod_with('Issue')
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
index a5e1957c096..86523bbd023 100644
--- a/app/models/issue/metrics.rb
+++ b/app/models/issue/metrics.rb
@@ -24,6 +24,10 @@ class Issue::Metrics < ApplicationRecord
private
def issue_assigned_to_list_label?
- issue.labels.any? { |label| label.lists.present? }
+ # Avoid another DB lookup when issue.labels are empty by adding a guard clause here
+ # We can't use issue.labels.empty? because that will cause a `Label Exists?` DB lookup
+ return false if issue.labels.length == 0 # rubocop:disable Style/ZeroLengthPredicate
+
+ issue.labels.includes(:lists).any? { |label| label.lists.present? }
end
end
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
index d62f0eb170c..d8fbd49d313 100644
--- a/app/models/issue_assignee.rb
+++ b/app/models/issue_assignee.rb
@@ -8,9 +8,9 @@ class IssueAssignee < ApplicationRecord
validates :assignee, uniqueness: { scope: :issue_id }
- scope :in_projects, ->(project_ids) { joins(:issue).where("issues.project_id in (?)", project_ids) }
+ scope :in_projects, ->(project_ids) { joins(:issue).where(issues: { project_id: project_ids }) }
scope :on_issues, ->(issue_ids) { where(issue_id: issue_ids) }
scope :for_assignee, ->(user) { where(assignee: user) }
end
-IssueAssignee.prepend_if_ee('EE::IssueAssignee')
+IssueAssignee.prepend_mod_with('IssueAssignee')
diff --git a/app/models/issue_link.rb b/app/models/issue_link.rb
index ba97874ed39..920586cc1ba 100644
--- a/app/models/issue_link.rb
+++ b/app/models/issue_link.rb
@@ -46,4 +46,4 @@ class IssueLink < ApplicationRecord
end
end
-IssueLink.prepend_if_ee('EE::IssueLink')
+IssueLink.prepend_mod_with('IssueLink')
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index 7483d04aab8..71ecbcf1c1a 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -13,4 +13,4 @@ class Iteration < ApplicationRecord
end
end
-Iteration.prepend_if_ee('::EE::Iteration')
+Iteration.prepend_mod_with('Iteration')
diff --git a/app/models/key.rb b/app/models/key.rb
index 131416d1bee..15b3c460b52 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -147,4 +147,4 @@ class Key < ApplicationRecord
end
end
-Key.prepend_if_ee('EE::Key')
+Key.prepend_mod_with('Key')
diff --git a/app/models/label.rb b/app/models/label.rb
index 26faaa90df3..a46d6bc5c0f 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -290,4 +290,4 @@ class Label < ApplicationRecord
end
end
-Label.prepend_if_ee('EE::Label')
+Label.prepend_mod_with('Label')
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 5ae1e88e14e..a466fe69300 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -9,4 +9,7 @@ class LabelLink < ApplicationRecord
validates :target, presence: true, unless: :importing?
validates :label, presence: true, unless: :importing?
+
+ scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
+ scope :with_remove_on_close_labels, -> { joins(:label).where(labels: { remove_on_close: true }) }
end
diff --git a/app/models/label_note.rb b/app/models/label_note.rb
index e90028ce835..19dede36abd 100644
--- a/app/models/label_note.rb
+++ b/app/models/label_note.rb
@@ -79,4 +79,4 @@ class LabelNote < SyntheticNote
end
end
-LabelNote.prepend_if_ee('EE::LabelNote')
+LabelNote.prepend_mod_with('LabelNote')
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index df1ad8ea281..25e90036a53 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -112,4 +112,4 @@ class LegacyDiffNote < Note
end
end
-LegacyDiffNote.prepend_if_ee('EE::LegacyDiffNote')
+LegacyDiffNote.prepend_mod_with('LegacyDiffNote')
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index d60baa299cb..b837b902e2d 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -50,4 +50,4 @@ class LfsObject < ApplicationRecord
end
end
-LfsObject.prepend_if_ee('EE::LfsObject')
+LfsObject.prepend_mod_with('LfsObject')
diff --git a/app/models/list.rb b/app/models/list.rb
index d72afbaee69..fba0e51bdf8 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -49,4 +49,4 @@ class List < ApplicationRecord
end
end
-List.prepend_if_ee('::EE::List')
+List.prepend_mod_with('List')
diff --git a/app/models/member.rb b/app/models/member.rb
index e978552592d..044b662e10f 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -84,15 +84,25 @@ class Member < ApplicationRecord
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_blocked = User.arel_table[:state].eq(:blocked)
- user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_blocked)
-
left_join_users
- .where(user_ok)
+ .where(user_is_blocked)
+ .where.not(is_external_invite)
.non_request
.non_minimal_access
.reorder(nil)
end
+ scope :connected_to_user, -> { where.not(user_id: nil) }
+
+ # This scope is exclusively used to get the members
+ # that can possibly have project_authorization records
+ # to projects/groups.
+ scope :authorizable, -> do
+ connected_to_user
+ .non_request
+ .non_minimal_access
+ end
+
# Like active, but without invites. For when a User is required.
scope :active_without_invites_and_requests, -> do
left_join_users
@@ -140,7 +150,8 @@ class Member < ApplicationRecord
scope :distinct_on_user_with_max_access_level, -> do
distinct_members = select('DISTINCT ON (user_id, invite_email) *')
.order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
- Member.from(distinct_members, :members)
+
+ from(distinct_members, :members)
end
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
@@ -560,4 +571,4 @@ class Member < ApplicationRecord
end
end
-Member.prepend_if_ee('EE::Member')
+Member.prepend_mod_with('Member')
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 0f9fdd230ff..b22a4fa9ef6 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -101,4 +101,4 @@ class GroupMember < Member
end
end
-GroupMember.prepend_if_ee('EE::GroupMember')
+GroupMember.prepend_mod_with('GroupMember')
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 9a86b3a3fd9..41ecc4cbf01 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -16,7 +16,7 @@ class ProjectMember < Member
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_namespaces, ->(groups) do
joins('INNER JOIN projects ON projects.id = members.source_id')
- .where('projects.namespace_id in (?)', groups.select(:id))
+ .where(projects: { namespace_id: groups.select(:id) })
end
scope :without_project_bots, -> do
@@ -69,7 +69,7 @@ class ProjectMember < Member
end
true
- rescue
+ rescue StandardError
false
end
@@ -154,4 +154,4 @@ class ProjectMember < Member
# rubocop: enable CodeReuse/ServiceClass
end
-ProjectMember.prepend_if_ee('EE::ProjectMember')
+ProjectMember.prepend_mod_with('ProjectMember')
diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb
index 88db7f63bd9..ba7e4b39989 100644
--- a/app/models/members_preloader.rb
+++ b/app/models/members_preloader.rb
@@ -10,10 +10,11 @@ class MembersPreloader
def preload_all
ActiveRecord::Associations::Preloader.new.preload(members, :user)
ActiveRecord::Associations::Preloader.new.preload(members, :source)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :webauthn_registrations)
+ ActiveRecord::Associations::Preloader.new.preload(members, :created_by)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :status)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :u2f_registrations)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn)
end
end
-MembersPreloader.prepend_if_ee('EE::MembersPreloader')
+MembersPreloader.prepend_mod_with('MembersPreloader')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e7f3762b9a3..aaef56418d2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -37,6 +37,7 @@ class MergeRequest < ApplicationRecord
SORTING_PREFERENCE_FIELD = :merge_requests_sort
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
+ 'Ci::CompareMetricsReportsService' => ->(project) { ::Gitlab::Ci::Features.merge_base_pipeline_for_metrics_comparison?(project) },
'Ci::CompareCodequalityReportsService' => ->(project) { true }
}.freeze
@@ -381,7 +382,7 @@ class MergeRequest < ApplicationRecord
scope :review_requested_to, ->(user) do
where(
reviewers_subquery
- .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user))
+ .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id))
.exists
)
end
@@ -389,7 +390,7 @@ class MergeRequest < ApplicationRecord
scope :no_review_requested_to, ->(user) do
where(
reviewers_subquery
- .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user))
+ .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id))
.exists
.not
)
@@ -1367,11 +1368,11 @@ class MergeRequest < ApplicationRecord
def environments_for(current_user, latest: false)
return [] unless diff_head_commit
- envs = EnvironmentsByDeploymentsFinder.new(target_project, current_user,
+ envs = Environments::EnvironmentsByDeploymentsFinder.new(target_project, current_user,
ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute
if source_project
- envs.concat EnvironmentsByDeploymentsFinder.new(source_project, current_user,
+ envs.concat Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user,
ref: source_branch, commit: diff_head_commit, find_latest: latest).execute
end
@@ -1741,7 +1742,7 @@ class MergeRequest < ApplicationRecord
if project.resolve_outdated_diff_discussions?
MergeRequests::ResolvedDiscussionNotificationService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.execute(self)
end
end
@@ -1899,6 +1900,12 @@ class MergeRequest < ApplicationRecord
diff_stats.map(&:path).include?(project.ci_config_path_or_default)
end
+ def context_commits_diff
+ strong_memoize(:context_commits_diff) do
+ ContextCommitsDiff.new(self)
+ end
+ end
+
private
def missing_report_error(report_type)
@@ -1948,4 +1955,4 @@ class MergeRequest < ApplicationRecord
end
end
-MergeRequest.prepend_if_ee('::EE::MergeRequest')
+MergeRequest.prepend_mod_with('MergeRequest')
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 5c611da0684..b9460afa8e7 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -31,4 +31,4 @@ class MergeRequest::Metrics < ApplicationRecord
end
end
-MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')
+MergeRequest::Metrics.prepend_mod_with('MergeRequest::Metrics')
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index 73f8fe77b04..86bf950ae19 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -6,5 +6,5 @@ class MergeRequestAssignee < ApplicationRecord
validates :assignee, uniqueness: { scope: :merge_request_id }
- scope :in_projects, ->(project_ids) { joins(:merge_request).where("merge_requests.target_project_id in (?)", project_ids) }
+ scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
end
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index 6f15df1b70f..8abedd26b06 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -16,4 +16,8 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
def self.bulk_insert(*args)
Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
+
+ def path
+ new_path.presence || old_path
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index fb873ddbbab..2dc6796732f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -113,14 +113,29 @@ class MergeRequestDiff < ApplicationRecord
joins(merge_request: :metrics).where(condition)
end
+ # This scope uses LATERAL JOIN to find the most recent MR diff association for the given merge requests.
+ # To avoid joining the merge_requests table, we build an in memory table using the merge request ids.
+ # Example:
+ # SELECT ...
+ # FROM (VALUES (MR_ID_1),(MR_ID_2)) merge_requests (id)
+ # INNER JOIN LATERAL (...)
scope :latest_diff_for_merge_requests, -> (merge_requests) do
- inner_select = MergeRequestDiff
- .default_scoped
- .distinct
- .select("FIRST_VALUE(id) OVER (PARTITION BY merge_request_id ORDER BY created_at DESC) as id")
- .where(merge_request: merge_requests)
+ mrs = Array(merge_requests)
+ return MergeRequestDiff.none if mrs.empty?
- joins("INNER JOIN (#{inner_select.to_sql}) latest_diffs ON latest_diffs.id = merge_request_diffs.id")
+ merge_request_table = MergeRequest.arel_table
+ merge_request_diff_table = MergeRequestDiff.arel_table
+
+ join_query = MergeRequestDiff
+ .where(merge_request_table[:id].eq(merge_request_diff_table[:merge_request_id]))
+ .order(created_at: :desc)
+ .limit(1)
+
+ mr_id_list = mrs.map { |mr| "(#{Integer(mr.id)})" }.join(",")
+
+ MergeRequestDiff
+ .from("(VALUES #{mr_id_list}) merge_requests (id)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{MergeRequestDiff.table_name} ON TRUE")
.includes(:merge_request_diff_commits)
end
@@ -665,10 +680,6 @@ class MergeRequestDiff < ApplicationRecord
opening_external_diff do
collection = merge_request_diff_files
- if options[:include_context_commits]
- collection += merge_request.merge_request_context_commit_diff_files
- end
-
if paths = options[:paths]
collection = collection.where('old_path IN (?) OR new_path IN (?)', paths, paths)
end
@@ -743,7 +754,6 @@ class MergeRequestDiff < ApplicationRecord
end
def reorder_diff_files!
- return unless sort_diffs?
return if sorted? || merge_request_diff_files.empty?
diff_files = sort_diffs(merge_request_diff_files)
@@ -762,14 +772,8 @@ class MergeRequestDiff < ApplicationRecord
end
def sort_diffs(diffs)
- return diffs unless sort_diffs?
-
Gitlab::Diff::FileCollectionSorter.new(diffs).sort
end
-
- def sort_diffs?
- Feature.enabled?(:sort_diffs, project, default_enabled: :yaml)
- end
end
-MergeRequestDiff.prepend_if_ee('EE::MergeRequestDiff')
+MergeRequestDiff.prepend_mod_with('MergeRequestDiff')
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 4cf0e423a15..16090f0ebfa 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -7,7 +7,7 @@ class Milestone < ApplicationRecord
include FromUnion
include Importable
- prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
class Predefined
ALL = [::Timebox::None, ::Timebox::Any, ::Timebox::Started, ::Timebox::Upcoming].freeze
@@ -94,7 +94,7 @@ class Milestone < ApplicationRecord
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct
+ User.joins(assigned_issues: :milestone).where(milestones: { id: id }).distinct
end
def self.sort_by_attribute(method)
diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb
index c6b5a967af9..93ad961ca51 100644
--- a/app/models/milestone_release.rb
+++ b/app/models/milestone_release.rb
@@ -19,4 +19,4 @@ class MilestoneRelease < ApplicationRecord
end
end
-MilestoneRelease.prepend_if_ee('EE::MilestoneRelease')
+MilestoneRelease.prepend_mod_with('MilestoneRelease')
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 455429608b4..8f03c6145cb 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -14,6 +14,7 @@ class Namespace < ApplicationRecord
include IgnorableColumns
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
+ include EachBatch
ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
@@ -88,8 +89,12 @@ class Namespace < ApplicationRecord
after_update :move_dir, if: :saved_change_to_path_or_parent?
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
+ after_commit :expire_child_caches, on: :update, if: -> {
+ Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml) &&
+ saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
+ }
- scope :for_user, -> { where('type IS NULL') }
+ scope :for_user, -> { where(type: nil) }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
@@ -198,7 +203,7 @@ class Namespace < ApplicationRecord
end
def any_project_has_container_registry_tags?
- all_projects.any?(&:has_container_registry_tags?)
+ all_projects.includes(:container_repositories).any?(&:has_container_registry_tags?)
end
def first_project_with_container_registry_tags
@@ -420,8 +425,22 @@ class Namespace < ApplicationRecord
created_at >= 90.days.ago
end
+ def issue_repositioning_disabled?
+ Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
+ end
+
private
+ def expire_child_caches
+ Namespace.where(id: descendants).each_batch do |namespaces|
+ namespaces.touch_all
+ end
+
+ all_projects.each_batch do |projects|
+ projects.touch_all
+ end
+ end
+
def all_projects_with_pages
if all_projects.pages_metadata_not_migrated.exists?
Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation(
@@ -490,4 +509,4 @@ class Namespace < ApplicationRecord
end
end
-Namespace.prepend_if_ee('EE::Namespace')
+Namespace.prepend_mod_with('Namespace')
diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb
index a2064e020b3..881b2f3acb3 100644
--- a/app/models/namespace/package_setting.rb
+++ b/app/models/namespace/package_setting.rb
@@ -6,13 +6,15 @@ class Namespace::PackageSetting < ApplicationRecord
PackageSettingNotImplemented = Class.new(StandardError)
- PACKAGES_WITH_SETTINGS = %w[maven].freeze
+ PACKAGES_WITH_SETTINGS = %w[maven generic].freeze
belongs_to :namespace, inverse_of: :package_setting_relation
validates :namespace, presence: true
validates :maven_duplicates_allowed, inclusion: { in: [true, false] }
validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
+ validates :generic_duplicates_allowed, inclusion: { in: [true, false] }
+ validates :generic_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
class << self
def duplicates_allowed?(package)
@@ -22,7 +24,7 @@ class Namespace::PackageSetting < ApplicationRecord
duplicates_allowed = package.package_settings["#{package.package_type}_duplicates_allowed"]
regex = ::Gitlab::UntrustedRegexp.new("\\A#{package.package_settings["#{package.package_type}_duplicate_exception_regex"]}\\z")
- duplicates_allowed || regex.match?(package.name)
+ duplicates_allowed || regex.match?(package.name) || regex.match?(package.version)
end
end
end
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 0c91ae760b2..73061b78637 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -70,4 +70,4 @@ class Namespace::RootStorageStatistics < ApplicationRecord
end
end
-Namespace::RootStorageStatistics.prepend_if_ee('EE::Namespace::RootStorageStatistics')
+Namespace::RootStorageStatistics.prepend_mod_with('Namespace::RootStorageStatistics')
diff --git a/app/models/namespace/traversal_hierarchy.rb b/app/models/namespace/traversal_hierarchy.rb
index 28cf55f7486..093b7dae246 100644
--- a/app/models/namespace/traversal_hierarchy.rb
+++ b/app/models/namespace/traversal_hierarchy.rb
@@ -20,7 +20,7 @@ class Namespace
end
def initialize(root)
- raise StandardError.new('Must specify a root node') if root.parent_id
+ raise StandardError, 'Must specify a root node' if root.parent_id
@root = root
end
@@ -34,20 +34,23 @@ class Namespace
sql = """
UPDATE namespaces
SET traversal_ids = cte.traversal_ids
- FROM (#{recursive_traversal_ids(lock: true)}) as cte
+ FROM (#{recursive_traversal_ids}) as cte
WHERE namespaces.id = cte.id
AND namespaces.traversal_ids <> cte.traversal_ids
"""
- Namespace.connection.exec_query(sql)
+ Namespace.transaction do
+ @root.lock!
+ Namespace.connection.exec_query(sql)
+ end
rescue ActiveRecord::Deadlocked
db_deadlock_counter.increment(source: 'Namespace#sync_traversal_ids!')
raise
end
# Identify all incorrect traversal_ids in the current namespace hierarchy.
- def incorrect_traversal_ids(lock: false)
+ def incorrect_traversal_ids
Namespace
- .joins("INNER JOIN (#{recursive_traversal_ids(lock: lock)}) as cte ON namespaces.id = cte.id")
+ .joins("INNER JOIN (#{recursive_traversal_ids}) as cte ON namespaces.id = cte.id")
.where('namespaces.traversal_ids <> cte.traversal_ids')
end
@@ -58,13 +61,10 @@ class Namespace
#
# Note that the traversal_ids represent a calculated traversal path for the
# namespace and not the value stored within the traversal_ids attribute.
- #
- # Optionally locked with FOR UPDATE to ensure isolation between concurrent
- # updates of the heirarchy.
- def recursive_traversal_ids(lock: false)
+ def recursive_traversal_ids
root_id = Integer(@root.id)
- sql = <<~SQL
+ <<~SQL
WITH RECURSIVE cte(id, traversal_ids, cycle) AS (
VALUES(#{root_id}, ARRAY[#{root_id}], false)
UNION ALL
@@ -74,10 +74,6 @@ class Namespace
)
SELECT id, traversal_ids FROM cte
SQL
-
- sql += ' FOR UPDATE' if lock
-
- sql
end
# This is essentially Namespace#root_ancestor which will soon be rewritten
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index d21f9632e18..75b8169b58e 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -45,4 +45,4 @@ class NamespaceSetting < ApplicationRecord
end
end
-NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')
+NamespaceSetting.prepend_mod_with('NamespaceSetting')
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 294ef83b9b4..a1711bc5ee0 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -41,6 +41,7 @@ module Namespaces
UnboundedSearch = Class.new(StandardError)
included do
+ before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
@@ -52,15 +53,30 @@ module Namespaces
end
def use_traversal_ids?
- Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+
+ traversal_ids.present?
end
def self_and_descendants
- if use_traversal_ids?
- lineage(self)
- else
- super
- end
+ return super unless use_traversal_ids?
+
+ lineage(top: self)
+ end
+
+ def descendants
+ return super unless use_traversal_ids?
+
+ self_and_descendants.where.not(id: id)
+ end
+
+ def ancestors(hierarchy_order: nil)
+ return super() unless use_traversal_ids?
+ return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
+
+ return self.class.none if parent_id.blank?
+
+ lineage(bottom: parent, hierarchy_order: hierarchy_order)
end
private
@@ -75,6 +91,23 @@ module Namespaces
Namespace::TraversalHierarchy.for_namespace(root_ancestor).sync_traversal_ids!
end
+ # Lock the root of the hierarchy we just left, and lock the root of the hierarchy
+ # we just joined. In most cases the two hierarchies will be the same.
+ def lock_both_roots
+ parent_ids = [
+ parent_id_was || self.id,
+ parent_id || self.id
+ ].compact
+
+ roots = Gitlab::ObjectHierarchy
+ .new(Namespace.where(id: parent_ids))
+ .base_and_ancestors
+ .reorder(nil)
+ .where(parent_id: nil)
+
+ Namespace.lock.select(:id).where(id: roots).order(id: :asc).load
+ end
+
# Make sure we drop the STI `type = 'Group'` condition for better performance.
# Logically equivalent so long as hierarchies remain homogeneous.
def without_sti_condition
@@ -82,29 +115,29 @@ module Namespaces
end
# Search this namespace's lineage. Bound inclusively by top node.
- def lineage(top)
- raise UnboundedSearch.new('Must bound search by a top') unless top
+ def lineage(top: nil, bottom: nil, hierarchy_order: nil)
+ raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
- without_sti_condition
- .traversal_ids_contains(latest_traversal_ids(top))
- end
+ skope = without_sti_condition
- # traversal_ids are a cached value.
- #
- # The traversal_ids value in a loaded object can become stale when compared
- # to the database value. For example, if you load a hierarchy and then move
- # a group, any previously loaded descendant objects will have out of date
- # traversal_ids.
- #
- # To solve this problem, we never depend on the object's traversal_ids
- # value. We always query the database first with a sub-select for the
- # latest traversal_ids.
- #
- # Note that ActiveRecord will cache query results. You can avoid this by
- # using `Model.uncached { ... }`
- def latest_traversal_ids(namespace = self)
- without_sti_condition.where('id = (?)', namespace)
- .select('traversal_ids as latest_traversal_ids')
+ if top
+ skope = skope.traversal_ids_contains("{#{top.id}}")
+ end
+
+ if bottom
+ skope = skope.where(id: bottom.traversal_ids[0..-1])
+ end
+
+ # The original `with_depth` attribute in ObjectHierarchy increments as you
+ # walk away from the "base" namespace. This direction changes depending on
+ # if you are walking up the ancestors or down the descendants.
+ if hierarchy_order
+ depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
+ skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
+ .order(depth: hierarchy_order)
+ end
+
+ skope
end
end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 9da454125eb..560ff861105 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -27,7 +27,7 @@ module Network
@project
.notes
- .where('noteable_type = ?', 'Commit')
+ .where(noteable_type: 'Commit')
.group('notes.commit_id')
.select('notes.commit_id, count(notes.id) as note_count')
.each do |item|
diff --git a/app/models/note.rb b/app/models/note.rb
index 3e560a09fbd..ae4a8859d4d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -107,6 +107,7 @@ class Note < ApplicationRecord
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
+ scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
@@ -319,7 +320,7 @@ class Note < ApplicationRecord
return commit if for_commit?
super
- rescue
+ rescue StandardError
# Temp fix to prevent app crash
# if note commit id doesn't exist
nil
@@ -495,7 +496,7 @@ class Note < ApplicationRecord
noteable&.expire_note_etag_cache
end
- def touch(*args)
+ def touch(*args, **kwargs)
# We're not using an explicit transaction here because this would in all
# cases result in all future queries going to the primary, even if no writes
# are performed.
@@ -638,4 +639,4 @@ class Note < ApplicationRecord
end
end
-Note.prepend_if_ee('EE::Note')
+Note.prepend_mod_with('Note')
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 3d049336d44..4323f89865a 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -118,4 +118,4 @@ class NotificationSetting < ApplicationRecord
end
end
-NotificationSetting.prepend_if_ee('EE::NotificationSetting')
+NotificationSetting.prepend_mod_with('NotificationSetting')
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index be3f719ddb3..537543a7ff0 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -97,7 +97,7 @@ module Operations
issues = ::Issue
.select('issues.*, operations_feature_flags_issues.id AS link_id')
.joins(:feature_flag_issues)
- .where('operations_feature_flags_issues.feature_flag_id = ?', id)
+ .where(operations_feature_flags_issues: { feature_flag_id: id })
.order('operations_feature_flags_issues.id ASC')
.includes(preload)
diff --git a/app/models/packages.rb b/app/models/packages.rb
index e14c9290093..19490d23ce4 100644
--- a/app/models/packages.rb
+++ b/app/models/packages.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
module Packages
+ DuplicatePackageError = Class.new(StandardError)
+
def self.table_name_prefix
'packages_'
end
diff --git a/app/models/packages/debian/group_distribution.rb b/app/models/packages/debian/group_distribution.rb
index eea7acacc96..50c1ec9f163 100644
--- a/app/models/packages/debian/group_distribution.rb
+++ b/app/models/packages/debian/group_distribution.rb
@@ -6,4 +6,14 @@ class Packages::Debian::GroupDistribution < ApplicationRecord
end
include Packages::Debian::Distribution
+
+ def packages
+ Packages::Package
+ .for_projects(group.all_projects.public_only)
+ .with_debian_codename(codename)
+ end
+
+ def package_files
+ ::Packages::PackageFile.for_package_ids(packages.select(:id))
+ end
end
diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb
index 22f1008b3b5..5ac60d789b3 100644
--- a/app/models/packages/debian/project_distribution.rb
+++ b/app/models/packages/debian/project_distribution.rb
@@ -5,8 +5,9 @@ class Packages::Debian::ProjectDistribution < ApplicationRecord
:project
end
+ include Packages::Debian::Distribution
+
has_many :publications, class_name: 'Packages::Debian::Publication', inverse_of: :distribution, foreign_key: :distribution_id
has_many :packages, class_name: 'Packages::Package', through: :publications
-
- include Packages::Debian::Distribution
+ has_many :package_files, class_name: 'Packages::PackageFile', through: :packages
end
diff --git a/app/models/packages/go/module.rb b/app/models/packages/go/module.rb
index b38b691ed6c..00d51c21881 100644
--- a/app/models/packages/go/module.rb
+++ b/app/models/packages/go/module.rb
@@ -18,8 +18,8 @@ module Packages
end
def version_by(ref: nil, commit: nil)
- raise ArgumentError.new 'no filter specified' unless ref || commit
- raise ArgumentError.new 'ref and commit are mutually exclusive' if ref && commit
+ raise ArgumentError, 'no filter specified' unless ref || commit
+ raise ArgumentError, 'ref and commit are mutually exclusive' if ref && commit
if commit
return version_by_sha(commit) if commit.is_a? String
diff --git a/app/models/packages/go/module_version.rb b/app/models/packages/go/module_version.rb
index fd575e6c96c..c442b2416f1 100644
--- a/app/models/packages/go/module_version.rb
+++ b/app/models/packages/go/module_version.rb
@@ -17,15 +17,15 @@ module Packages
delegate :build, to: :@semver, allow_nil: true
def initialize(mod, type, commit, name: nil, semver: nil, ref: nil)
- raise ArgumentError.new("invalid type '#{type}'") unless VALID_TYPES.include? type
- raise ArgumentError.new("mod is required") unless mod
- raise ArgumentError.new("commit is required") unless commit
+ raise ArgumentError, "invalid type '#{type}'" unless VALID_TYPES.include? type
+ raise ArgumentError, "mod is required" unless mod
+ raise ArgumentError, "commit is required" unless commit
if type == :ref
- raise ArgumentError.new("ref is required") unless ref
+ raise ArgumentError, "ref is required" unless ref
elsif type == :pseudo
- raise ArgumentError.new("name is required") unless name
- raise ArgumentError.new("semver is required") unless semver
+ raise ArgumentError, "name is required" unless name
+ raise ArgumentError, "semver is required" unless semver
end
@mod = mod
diff --git a/app/models/packages/helm.rb b/app/models/packages/helm.rb
new file mode 100644
index 00000000000..e021b997bf5
--- /dev/null
+++ b/app/models/packages/helm.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Packages
+ module Helm
+ def self.table_name_prefix
+ 'packages_helm_'
+ end
+ end
+end
diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb
new file mode 100644
index 00000000000..1771003d1f9
--- /dev/null
+++ b/app/models/packages/helm/file_metadatum.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Packages
+ module Helm
+ class FileMetadatum < ApplicationRecord
+ self.primary_key = :package_file_id
+
+ belongs_to :package_file, inverse_of: :helm_file_metadatum
+
+ validates :package_file, presence: true
+ validate :valid_helm_package_type
+
+ validates :channel,
+ presence: true,
+ length: { maximum: 63 },
+ format: { with: Gitlab::Regex.helm_channel_regex }
+
+ validates :metadata,
+ json_schema: { filename: "helm_metadata" }
+
+ private
+
+ def valid_helm_package_type
+ return if package_file&.package&.helm?
+
+ errors.add(:package_file, _('Package type must be Helm'))
+ end
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index e510432be8f..36edf646658 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -6,6 +6,7 @@ class Packages::Package < ApplicationRecord
include Gitlab::Utils::StrongMemoize
DISPLAYABLE_STATUSES = [:default, :error].freeze
+ INSTALLABLE_STATUSES = [:default].freeze
belongs_to :project
belongs_to :creator, class_name: 'User'
@@ -47,8 +48,10 @@ class Packages::Package < ApplicationRecord
validate :package_already_taken, if: :npm?
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
+ validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm?
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
+ validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module?
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package?
validates :name, inclusion: { in: %w[incoming] }, if: :debian_incoming?
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
@@ -56,7 +59,8 @@ class Packages::Package < ApplicationRecord
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang?
- validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? }
+ validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :helm?
+ validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? || terraform_module? }
validates :version,
presence: true,
@@ -70,10 +74,11 @@ class Packages::Package < ApplicationRecord
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5,
composer: 6, generic: 7, golang: 8, debian: 9,
- rubygems: 10 }
+ rubygems: 10, helm: 11, terraform_module: 12 }
enum status: { default: 0, hidden: 1, processing: 2, error: 3 }
+ scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
scope :with_normalized_pypi_name, ->(name) { where("LOWER(regexp_replace(name, '[-_.]+', '-', 'g')) = ?", name.downcase) }
@@ -81,8 +86,10 @@ class Packages::Package < ApplicationRecord
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
+ scope :without_package_type, ->(package_type) { where.not(package_type: package_type) }
scope :with_status, ->(status) { where(status: status) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
+ scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
@@ -110,25 +117,20 @@ class Packages::Package < ApplicationRecord
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) }
- scope :processed, -> do
- where.not(package_type: :nuget).or(
- where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME)
- )
- end
scope :preload_files, -> { preload(:package_files) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
scope :select_distinct_name, -> { select(:name).distinct }
# Sorting
- scope :order_created, -> { reorder('created_at ASC') }
- scope :order_created_desc, -> { reorder('created_at DESC') }
- scope :order_name, -> { reorder('name ASC') }
- scope :order_name_desc, -> { reorder('name DESC') }
- scope :order_version, -> { reorder('version ASC') }
- scope :order_version_desc, -> { reorder('version DESC') }
- scope :order_type, -> { reorder('package_type ASC') }
- scope :order_type_desc, -> { reorder('package_type DESC') }
+ scope :order_created, -> { reorder(created_at: :asc) }
+ scope :order_created_desc, -> { reorder(created_at: :desc) }
+ scope :order_name, -> { reorder(name: :asc) }
+ scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :order_version, -> { reorder(version: :asc) }
+ scope :order_version_desc, -> { reorder(version: :desc) }
+ scope :order_type, -> { reorder(package_type: :asc) }
+ scope :order_type_desc, -> { reorder(package_type: :desc) }
scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') }
scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') }
scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') }
@@ -137,14 +139,6 @@ class Packages::Package < ApplicationRecord
after_commit :update_composer_cache, on: :destroy, if: -> { composer? }
- def self.for_projects(projects)
- unless Feature.enabled?(:maven_packages_group_level_improvements, default_enabled: :yaml)
- return none unless projects.any?
- end
-
- where(project_id: projects)
- end
-
def self.only_maven_packages_with_path(path, use_cte: false)
if use_cte && Feature.enabled?(:maven_metadata_by_path_with_optimization_fence, default_enabled: :yaml)
# This is an optimization fence which assumes that looking up the Metadatum record by path (globally)
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 23a7144e2bb..3d8641ca2fa 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -5,7 +5,8 @@ class Packages::PackageFile < ApplicationRecord
delegate :project, :project_id, to: :package
delegate :conan_file_type, to: :conan_file_metadatum
- delegate :file_type, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
+ delegate :file_type, :component, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
+ delegate :channel, :metadata, to: :helm_file_metadatum, prefix: :helm
belongs_to :package
@@ -13,9 +14,11 @@ class Packages::PackageFile < ApplicationRecord
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
has_many :pipelines, through: :package_file_build_infos
has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum'
+ has_one :helm_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Helm::FileMetadatum'
accepts_nested_attributes_for :conan_file_metadatum
accepts_nested_attributes_for :debian_file_metadatum
+ accepts_nested_attributes_for :helm_file_metadatum
validates :package, presence: true
validates :file, presence: true
@@ -24,6 +27,7 @@ class Packages::PackageFile < ApplicationRecord
validates :file_name, uniqueness: { scope: :package }, if: -> { package&.pypi? }
scope :recent, -> { order(id: :desc) }
+ scope :for_package_ids, ->(ids) { where(package_id: ids) }
scope :with_file_name, ->(file_name) { where(file_name: file_name) }
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
@@ -41,7 +45,17 @@ class Packages::PackageFile < ApplicationRecord
scope :with_debian_file_type, ->(file_type) do
joins(:debian_file_metadatum)
- .where(packages_debian_file_metadata: { debian_file_type: ::Packages::Debian::FileMetadatum.debian_file_types[file_type] })
+ .where(packages_debian_file_metadata: { file_type: ::Packages::Debian::FileMetadatum.file_types[file_type] })
+ end
+
+ scope :with_debian_component_name, ->(component_name) do
+ joins(:debian_file_metadatum)
+ .where(packages_debian_file_metadata: { component: component_name })
+ end
+
+ scope :with_debian_architecture_name, ->(architecture_name) do
+ joins(:debian_file_metadatum)
+ .where(packages_debian_file_metadata: { architecture: architecture_name })
end
scope :with_conan_package_reference, ->(conan_package_reference) do
@@ -66,4 +80,4 @@ class Packages::PackageFile < ApplicationRecord
end
end
-Packages::PackageFile.prepend_if_ee('EE::Packages::PackageFile')
+Packages::PackageFile.prepend_mod_with('Packages::PackageFile')
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 3285a1f7f4c..17131cd736d 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -50,8 +50,6 @@ module Pages
def zip_source
return unless deployment&.file
- return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project, default_enabled: :yaml)
-
global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
{
@@ -64,17 +62,16 @@ module Pages
}
end
+ # TODO: remove support for legacy storage in 14.3 https://gitlab.com/gitlab-org/gitlab/-/issues/328712
+ # we support this till 14.3 to allow people to still use legacy storage if something goes very wrong
+ # on self-hosted installations, and we'll need some time to fix it
def legacy_source
- raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true)
+ return unless ::Settings.pages.local_store.enabled
{
type: 'file',
path: File.join(project.full_path, 'public/')
}
- rescue LegacyStorageDisabledError => e
- Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
-
- nil
end
end
end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 90cb8253b52..497f67993ae 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -21,9 +21,7 @@ module Pages
project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
end
- # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524
- # source can only be nil if pages_serve_from_legacy_storage FF is disabled
- # we can remove this filtering once we remove legacy storage
+ # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715
paths = paths.select(&:source)
paths.sort_by(&:prefix).reverse
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 4d60489e599..4668fc265a0 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -311,4 +311,4 @@ class PagesDomain < ApplicationRecord
end
end
-PagesDomain.prepend_if_ee('::EE::PagesDomain')
+PagesDomain.prepend_mod_with('PagesDomain')
diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb
index 411456cc237..8427176fa72 100644
--- a/app/models/pages_domain_acme_order.rb
+++ b/app/models/pages_domain_acme_order.rb
@@ -14,7 +14,7 @@ class PagesDomainAcmeOrder < ApplicationRecord
attr_encrypted :private_key,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: true
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index ad2f4525171..732ed0b7bb3 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -55,7 +55,7 @@ class PersonalAccessToken < ApplicationRecord
begin
Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
- rescue => ex
+ rescue StandardError => ex
logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{ex.class}"
encrypted_token
end
@@ -110,4 +110,4 @@ class PersonalAccessToken < ApplicationRecord
end
end
-PersonalAccessToken.prepend_if_ee('EE::PersonalAccessToken')
+PersonalAccessToken.prepend_mod_with('PersonalAccessToken')
diff --git a/app/models/plan.rb b/app/models/plan.rb
index 6a7f32a5d5f..f3ef04315f8 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -39,4 +39,4 @@ class Plan < ApplicationRecord
end
end
-Plan.prepend_if_ee('EE::Plan')
+Plan.prepend_mod_with('Plan')
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 94992adfd1e..78cddaa1302 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -115,4 +115,4 @@ class PoolRepository < ApplicationRecord
end
end
-PoolRepository.prepend_if_ee('EE::PoolRepository')
+PoolRepository.prepend_mod_with('PoolRepository')
diff --git a/app/models/preloaders/labels_preloader.rb b/app/models/preloaders/labels_preloader.rb
index 427f2869aac..bb3206f5399 100644
--- a/app/models/preloaders/labels_preloader.rb
+++ b/app/models/preloaders/labels_preloader.rb
@@ -31,4 +31,4 @@ module Preloaders
end
end
-Preloaders::LabelsPreloader.prepend_if_ee('EE::Preloaders::LabelsPreloader')
+Preloaders::LabelsPreloader.prepend_mod_with('Preloaders::LabelsPreloader')
diff --git a/app/models/project.rb b/app/models/project.rb
index f03e5293b58..9d572b7e2f8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,6 +19,7 @@ class Project < ApplicationRecord
include Presentable
include HasRepository
include HasWiki
+ include HasIntegrations
include CanMoveRepositoryStorage
include Routable
include GroupDescendant
@@ -33,7 +34,6 @@ class Project < ApplicationRecord
include OptionallySearch
include FromUnion
include IgnorableColumns
- include Integration
include Repositories::CanHousekeepRepository
include EachBatch
include GitlabRoutingHelper
@@ -104,16 +104,13 @@ class Project < ApplicationRecord
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
- after_create :create_project_feature, unless: :project_feature
+ after_create -> { create_or_load_association(:project_feature) }
- after_create :create_ci_cd_settings,
- unless: :ci_cd_settings
+ after_create -> { create_or_load_association(:ci_cd_settings) }
- after_create :create_container_expiration_policy,
- unless: :container_expiration_policy
+ after_create -> { create_or_load_association(:container_expiration_policy) }
- after_create :create_pages_metadatum,
- unless: :pages_metadatum
+ after_create -> { create_or_load_association(:pages_metadatum) }
after_create :set_timestamps_for_create
after_update :update_forks_visibility_level
@@ -131,7 +128,41 @@ class Project < ApplicationRecord
after_initialize :use_hashed_storage
after_create :check_repository_absence!
- acts_as_ordered_taggable
+ acts_as_ordered_taggable_on :topics
+ # The 'tag_list' alias and the 'has_many' associations are required during the 'tags -> topics' migration
+ # TODO: eliminate 'tag_list', 'topic_taggings' and 'tags' in the further process of the migration
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331081
+ alias_attribute :tag_list, :topic_list
+ has_many :topic_taggings, -> { includes(:tag).order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ as: :taggable,
+ class_name: 'ActsAsTaggableOn::Tagging',
+ after_add: :dirtify_tag_list,
+ after_remove: :dirtify_tag_list
+ has_many :topics, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ class_name: 'ActsAsTaggableOn::Tag',
+ through: :topic_taggings,
+ source: :tag
+ has_many :tags, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ class_name: 'ActsAsTaggableOn::Tag',
+ through: :topic_taggings,
+ source: :tag
+
+ # Overwriting 'topic_list' and 'topic_list=' is necessary to ensure functionality during the background migration [1].
+ # [1] https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61237
+ # TODO: remove 'topic_list' and 'topic_list=' once the background migration is complete
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331081
+ def topic_list
+ # Return both old topics (context 'tags') and new topics (context 'topics')
+ tag_list_on('tags') + tag_list_on('topics')
+ end
+
+ def topic_list=(new_tags)
+ # Old topics with context 'tags' are added as new topics with context 'topics'
+ super(new_tags)
+
+ # Remove old topics with context 'tags'
+ set_tag_list_on('tags', '')
+ end
attr_accessor :old_path_with_namespace
attr_accessor :template_name
@@ -151,26 +182,26 @@ class Project < ApplicationRecord
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards
- # Project services
- has_one :campfire_service
- has_one :datadog_service
+ # Project integrations
+ has_one :asana_service, class_name: 'Integrations::Asana'
+ has_one :assembla_service, class_name: 'Integrations::Assembla'
+ has_one :bamboo_service, class_name: 'Integrations::Bamboo'
+ has_one :campfire_service, class_name: 'Integrations::Campfire'
+ has_one :confluence_service, class_name: 'Integrations::Confluence'
+ has_one :datadog_service, class_name: 'Integrations::Datadog'
+ has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
has_one :discord_service
has_one :drone_ci_service
- has_one :emails_on_push_service
has_one :ewm_service
has_one :pipelines_email_service
has_one :irker_service
has_one :pivotaltracker_service
- has_one :hipchat_service
has_one :flowdock_service
- has_one :assembla_service
- has_one :asana_service
has_one :mattermost_slash_commands_service
has_one :mattermost_service
has_one :slack_slash_commands_service
has_one :slack_service
has_one :buildkite_service
- has_one :bamboo_service
has_one :teamcity_service
has_one :pushover_service
has_one :jenkins_service
@@ -179,7 +210,6 @@ class Project < ApplicationRecord
has_one :youtrack_service
has_one :custom_issue_tracker_service
has_one :bugzilla_service
- has_one :confluence_service
has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service
@@ -227,7 +257,7 @@ class Project < ApplicationRecord
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
- has_many :services
+ has_many :integrations
has_many :events
has_many :milestones
has_many :iterations
@@ -338,7 +368,8 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
- has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
+ has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :project
+ has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', inverse_of: :project
has_many :external_pull_requests, inverse_of: :project
@@ -371,6 +402,8 @@ class Project < ApplicationRecord
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
+ has_many :timelogs
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -528,7 +561,7 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
- scope :with_active_jira_services, -> { joins(:services).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
+ scope :with_active_jira_services, -> { joins(:integrations).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) }
@@ -619,7 +652,7 @@ class Project < ApplicationRecord
mount_uploader :bfg_object_map, AttachmentUploader
def self.with_api_entity_associations
- preload(:project_feature, :route, :tags, :group, namespace: [:route, :owner])
+ preload(:project_feature, :route, :tags, :group, :timelogs, namespace: [:route, :owner])
end
def self.with_web_entity_associations
@@ -832,6 +865,10 @@ class Project < ApplicationRecord
super
end
+ def parent_loaded?
+ association(:namespace).loaded?
+ end
+
def project_setting
super.presence || build_project_setting
end
@@ -1005,7 +1042,7 @@ class Project < ApplicationRecord
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)
- latest_successful_build_for_ref(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
+ latest_successful_build_for_ref(job_name, ref) || raise(ActiveRecord::RecordNotFound, "Couldn't find job #{job_name}")
end
def latest_pipeline(ref = default_branch, sha = nil)
@@ -1098,7 +1135,7 @@ class Project < ApplicationRecord
else
super
end
- rescue
+ rescue StandardError
super
end
@@ -1342,7 +1379,7 @@ class Project < ApplicationRecord
return unless has_external_issue_tracker?
- @external_issue_tracker ||= services.external_issue_trackers.first
+ @external_issue_tracker ||= integrations.external_issue_trackers.first
end
def external_references_supported?
@@ -1358,11 +1395,11 @@ class Project < ApplicationRecord
return unless has_external_wiki?
- @external_wiki ||= services.external_wikis.first
+ @external_wiki ||= integrations.external_wikis.first
end
def find_or_initialize_services
- available_services_names = Service.available_services_names - disabled_services
+ available_services_names = Integration.available_services_names - disabled_services
available_services_names.map do |service_name|
find_or_initialize_service(service_name)
@@ -1378,7 +1415,7 @@ class Project < ApplicationRecord
def find_or_initialize_service(name)
return if disabled_services.include?(name)
- find_service(services, name) || build_from_instance_or_template(name) || build_service(name)
+ find_service(integrations, name) || build_from_instance_or_template(name) || build_service(name)
end
# rubocop: disable CodeReuse/ServiceClass
@@ -1391,7 +1428,7 @@ class Project < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def ci_services
- services.where(category: :ci)
+ integrations.where(category: :ci)
end
def ci_service
@@ -1399,7 +1436,7 @@ class Project < ApplicationRecord
end
def monitoring_services
- services.where(category: :monitoring)
+ integrations.where(category: :monitoring)
end
def monitoring_service
@@ -1477,8 +1514,8 @@ class Project < ApplicationRecord
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
- services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
- service.async_execute(data)
+ integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
+ integration.async_execute(data)
end
end
end
@@ -1488,7 +1525,7 @@ class Project < ApplicationRecord
end
def has_active_services?(hooks_scope = :push_hooks)
- services.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
+ integrations.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
end
def feature_usage
@@ -1560,7 +1597,7 @@ class Project < ApplicationRecord
repository.after_create
true
- rescue => err
+ rescue StandardError => err
Gitlab::ErrorTracking.track_exception(err, project: { id: id, full_path: full_path, disk_path: disk_path })
errors.add(:base, _('Failed to create repository'))
false
@@ -2417,7 +2454,7 @@ class Project < ApplicationRecord
end
def access_request_approvers_to_be_notified
- members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ members.maintainers.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def pages_lookup_path(trim_prefix: nil, domain: nil)
@@ -2529,12 +2566,14 @@ class Project < ApplicationRecord
namespace.root_ancestor.all_projects
.joins(:packages)
.where.not(id: id)
- .merge(Packages::Package.with_name(package_name))
+ .merge(Packages::Package.default_scoped.with_name(package_name))
.exists?
end
- def default_branch_or_master
- default_branch || 'master'
+ def default_branch_or_main
+ return default_branch if default_branch
+
+ Gitlab::DefaultBranch.value(object: self)
end
def ci_config_path_or_default
@@ -2569,6 +2608,16 @@ class Project < ApplicationRecord
Feature.enabled?(:inherited_issuable_templates, self, default_enabled: :yaml)
end
+ def activity_path
+ Gitlab::Routing.url_helpers.activity_project_path(self)
+ end
+
+ def increment_statistic_value(statistic, delta)
+ return if pending_delete?
+
+ ProjectStatistics.increment_statistic(self, statistic, delta)
+ end
+
private
def set_container_registry_access_level
@@ -2591,22 +2640,22 @@ class Project < ApplicationRecord
def build_from_instance_or_template(name)
instance = find_service(services_instances, name)
- return Service.build_from_integration(instance, project_id: id) if instance
+ return Integration.build_from_integration(instance, project_id: id) if instance
template = find_service(services_templates, name)
- return Service.build_from_integration(template, project_id: id) if template
+ return Integration.build_from_integration(template, project_id: id) if template
end
def build_service(name)
- "#{name}_service".classify.constantize.new(project_id: id)
+ Integration.service_name_to_model(name).new(project_id: id)
end
def services_templates
- @services_templates ||= Service.for_template
+ @services_templates ||= Integration.for_template
end
def services_instances
- @services_instances ||= Service.for_instance
+ @services_instances ||= Integration.for_instance
end
def closest_namespace_setting(name)
@@ -2664,7 +2713,7 @@ class Project < ApplicationRecord
def cross_namespace_reference?(from)
case from
when Project
- namespace != from.namespace
+ namespace_id != from.namespace_id
when Namespace
namespace != from
when User
@@ -2743,11 +2792,11 @@ class Project < ApplicationRecord
end
def cache_has_external_wiki
- update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
+ update_column(:has_external_wiki, integrations.external_wikis.any?) if Gitlab::Database.read_write?
end
def cache_has_external_issue_tracker
- update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
+ update_column(:has_external_issue_tracker, integrations.external_issue_trackers.any?) if Gitlab::Database.read_write?
end
def active_runners_with_tags
@@ -2759,4 +2808,4 @@ class Project < ApplicationRecord
end
end
-Project.prepend_if_ee('EE::Project')
+Project.prepend_mod_with('Project')
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 2c3f70654f8..1fed166e4d0 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -31,4 +31,4 @@ class ProjectAuthorization < ApplicationRecord
end
end
-ProjectAuthorization.prepend_if_ee('::EE::ProjectAuthorization')
+ProjectAuthorization.prepend_mod_with('ProjectAuthorization')
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 31be0759cd0..c0c2ea42d46 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -33,4 +33,4 @@ class ProjectCiCdSetting < ApplicationRecord
end
end
-ProjectCiCdSetting.prepend_if_ee('EE::ProjectCiCdSetting')
+ProjectCiCdSetting.prepend_mod_with('ProjectCiCdSetting')
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 15f6bedfc2e..eb4ad327438 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -145,4 +145,4 @@ class ProjectFeature < ApplicationRecord
end
end
-ProjectFeature.prepend_if_ee('EE::ProjectFeature')
+ProjectFeature.prepend_mod_with('ProjectFeature')
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index 02051310af7..d993db860c3 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -45,4 +45,4 @@ class ProjectFeatureUsage < ApplicationRecord
end
end
-ProjectFeatureUsage.prepend_if_ee('EE::ProjectFeatureUsage')
+ProjectFeatureUsage.prepend_mod_with('ProjectFeatureUsage')
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index f065246e8af..d704f4c2c87 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -2,6 +2,7 @@
class ProjectGroupLink < ApplicationRecord
include Expirable
+ include EachBatch
belongs_to :project
belongs_to :group
@@ -49,4 +50,4 @@ class ProjectGroupLink < ApplicationRecord
end
end
-ProjectGroupLink.prepend_if_ee('EE::ProjectGroupLink')
+ProjectGroupLink.prepend_mod_with('ProjectGroupLink')
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 87ac6d38787..d374ee120d1 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -3,7 +3,7 @@
require 'carrierwave/orm/activerecord'
class ProjectImportData < ApplicationRecord
- prepend_if_ee('::EE::ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ prepend_mod_with('ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule
belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index 4bd3ffbea2f..633e669b5fc 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -105,4 +105,4 @@ class ProjectImportState < ApplicationRecord
end
end
-ProjectImportState.prepend_if_ee('EE::ProjectImportState')
+ProjectImportState.prepend_mod_with('ProjectImportState')
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
deleted file mode 100644
index f31bf931a41..00000000000
--- a/app/models/project_services/asana_service.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require 'asana'
-
-class AsanaService < Service
- include ActionView::Helpers::UrlHelper
-
- prop_accessor :api_key, :restrict_to_branch
- validates :api_key, presence: true, if: :activated?
-
- def title
- 'Asana'
- end
-
- def description
- s_('AsanaService|Add commit messages as comments to Asana tasks')
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
- s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'asana'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'api_key',
- title: 'API key',
- help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
- # Example Personal Access Token from Asana docs
- placeholder: '0/68a9e79b868c6789e79a124c30b0',
- required: true
- },
- {
- type: 'text',
- name: 'restrict_to_branch',
- title: 'Restrict to branch (optional)',
- help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
- }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def client
- @_client ||= begin
- Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
- end
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- # check the branch restriction is poplulated and branch is not included
- branch = Gitlab::Git.ref_name(data[:ref])
- branch_restriction = restrict_to_branch.to_s
- if branch_restriction.present? && branch_restriction.index(branch).nil?
- return
- end
-
- user = data[:user_name]
- project_name = project.full_name
-
- data[:commits].each do |commit|
- push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
- check_commit(commit[:message], push_msg)
- end
- end
-
- def check_commit(message, push_msg)
- # matches either:
- # - #1234
- # - https://app.asana.com/0/{project_gid}/{task_gid}
- # optionally preceded with:
- # - fix/ed/es/ing
- # - close/s/d
- # - closing
- issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
-
- message.scan(issue_finder).each do |tuple|
- # tuple will be
- # [ 'fix', 'id_from_url', 'id_from_pound' ]
- taskid = tuple[2] || tuple[1]
-
- begin
- task = Asana::Resources::Task.find_by_id(client, taskid)
- task.add_comment(text: "#{push_msg} #{message}")
-
- if tuple[0]
- task.update(completed: true)
- end
- rescue => e
- log_error(e.message)
- next
- end
- end
- end
-end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
deleted file mode 100644
index 8845fb99605..00000000000
--- a/app/models/project_services/assembla_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-class AssemblaService < Service
- prop_accessor :token, :subdomain
- validates :token, presence: true, if: :activated?
-
- def title
- 'Assembla'
- end
-
- def description
- _('Manage projects.')
- end
-
- def self.to_param
- 'assembla'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'subdomain', placeholder: '' }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
- Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
- end
-end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
deleted file mode 100644
index a892d1a4314..00000000000
--- a/app/models/project_services/bamboo_service.rb
+++ /dev/null
@@ -1,181 +0,0 @@
-# frozen_string_literal: true
-
-class BambooService < CiService
- include ActionView::Helpers::UrlHelper
- include ReactiveService
-
- prop_accessor :bamboo_url, :build_key, :username, :password
-
- validates :bamboo_url, presence: true, public_url: true, if: :activated?
- validates :build_key, presence: true, if: :activated?
- validates :username,
- presence: true,
- if: ->(service) { service.activated? && service.password }
- validates :password,
- presence: true,
- if: ->(service) { service.activated? && service.username }
-
- attr_accessor :response
-
- after_save :compose_service_hook, if: :activated?
- before_update :reset_password
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.save
- end
-
- def reset_password
- if bamboo_url_changed? && !password_touched?
- self.password = nil
- end
- end
-
- def title
- s_('BambooService|Atlassian Bamboo')
- end
-
- def description
- s_('BambooService|Use the Atlassian Bamboo CI/CD server with GitLab.')
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
- s_('BambooService|Use Atlassian Bamboo to run CI/CD pipelines. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'bamboo'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'bamboo_url',
- title: s_('BambooService|Bamboo URL'),
- placeholder: s_('https://bamboo.example.com'),
- help: s_('BambooService|Bamboo service root URL.'),
- required: true
- },
- {
- type: 'text',
- name: 'build_key',
- placeholder: s_('KEY'),
- help: s_('BambooService|Bamboo build plan key.'),
- required: true
- },
- {
- type: 'text',
- name: 'username',
- help: s_('BambooService|The user with API access to the Bamboo server.')
- },
- {
- type: 'password',
- name: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
- }
- ]
- end
-
- def build_page(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
- end
-
- def commit_status(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- get_path("updateAndBuild.action", { buildKey: build_key })
- end
-
- def calculate_reactive_cache(sha, ref)
- response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
-
- { build_page: read_build_page(response), commit_status: read_commit_status(response) }
- end
-
- private
-
- def get_build_result(response)
- return if response&.code != 200
-
- # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
- result = response.dig('results', 'results', 'result')
-
- # In case of multiple results, arbitrarily assume the last one is the most relevant.
- return result.last if result.is_a?(Array)
-
- result
- end
-
- def read_build_page(response)
- result = get_build_result(response)
- key =
- if result.blank?
- # If actual build link can't be determined, send user to build summary page.
- build_key
- else
- # If actual build link is available, go to build result page.
- result.dig('planResultKey', 'key')
- end
-
- build_url("browse/#{key}")
- end
-
- def read_commit_status(response)
- return :error unless response && (response.code == 200 || response.code == 404)
-
- result = get_build_result(response)
- status =
- if result.blank?
- 'Pending'
- else
- result.dig('buildState')
- end
-
- return :error unless status.present?
-
- if status.include?('Success')
- 'success'
- elsif status.include?('Failed')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
- end
-
- def try_get_path(path, query_params = {})
- params = build_get_params(query_params)
- params[:extra_log_info] = { project_id: project_id }
-
- Gitlab::HTTP.try_get(build_url(path), params)
- end
-
- def get_path(path, query_params = {})
- Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
- end
-
- def build_url(path)
- Gitlab::Utils.append_path(bamboo_url, path)
- end
-
- def build_get_params(query_params)
- params = { verify: false, query: query_params }
- return params if username.blank? && password.blank?
-
- query_params[:os_authType] = 'basic'
- params[:basic_auth] = basic_auth
- params
- end
-
- def basic_auth
- { username: username, password: password }
- end
-end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 4332db3e961..d1c56d2a4d5 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class BugzillaService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
def title
@@ -8,7 +10,12 @@ class BugzillaService < IssueTrackerService
end
def description
- s_('IssueTracker|Bugzilla issue tracker')
+ s_("IssueTracker|Use Bugzilla as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 53bb7b47b41..f2ea5066e37 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -68,7 +68,7 @@ class BuildkiteService < CiService
end
def description
- 'Buildkite is a platform for running fast, secure, and scalable continuous integration pipelines on your own infrastructure'
+ 'Run CI/CD pipelines with Buildkite.'
end
def self.to_param
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
deleted file mode 100644
index f2295a95b60..00000000000
--- a/app/models/project_services/builds_email_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-# This class is to be removed with 9.1
-# We should also by then remove BuildsEmailService from database
-class BuildsEmailService < Service
- def self.to_param
- 'builds_email'
- end
-
- def self.supported_events
- %w[]
- end
-end
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
deleted file mode 100644
index ad26e42a21b..00000000000
--- a/app/models/project_services/campfire_service.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-class CampfireService < Service
- prop_accessor :token, :subdomain, :room
- validates :token, presence: true, if: :activated?
-
- def title
- 'Campfire'
- end
-
- def description
- 'Simple web-based real-time group chat'
- end
-
- def self.to_param
- 'campfire'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'subdomain', placeholder: '' },
- { type: 'text', name: 'room', placeholder: '' }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- message = build_message(data)
- speak(self.room, message, auth)
- end
-
- private
-
- def base_uri
- @base_uri ||= "https://#{subdomain}.campfirenow.com"
- end
-
- def auth
- # use a dummy password, as explained in the Campfire API doc:
- # https://github.com/basecamp/campfire-api#authentication
- @auth ||= {
- basic_auth: {
- username: token,
- password: 'X'
- }
- }
- end
-
- # Post a message into a room, returns the message Hash in case of success.
- # Returns nil otherwise.
- # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
- def speak(room_name, message, auth)
- room = rooms(auth).find { |r| r["name"] == room_name }
- return unless room
-
- path = "/room/#{room["id"]}/speak.json"
- body = {
- body: {
- message: {
- type: 'TextMessage',
- body: message
- }
- }
- }
- res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body))
- res.code == 201 ? res : nil
- end
-
- # Returns a list of rooms, or [].
- # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
- def rooms(auth)
- res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth)
- res.code == 200 ? res["rooms"] : []
- end
-
- def build_message(push)
- ref = Gitlab::Git.ref_name(push[:ref])
- before = push[:before]
- after = push[:after]
-
- message = []
- message << "[#{project.full_name}] "
- message << "#{push[:user_name]} "
-
- if Gitlab::Git.blank_ref?(before)
- message << "pushed new branch #{ref} \n"
- elsif Gitlab::Git.blank_ref?(after)
- message << "removed branch #{ref} \n"
- else
- message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
- message << "#{project.web_url}/compare/#{before}...#{after}"
- end
-
- message.join
- end
-end
diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb
deleted file mode 100644
index c8913775843..00000000000
--- a/app/models/project_services/chat_message/alert_message.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class AlertMessage < BaseMessage
- attr_reader :title
- attr_reader :alert_url
- attr_reader :severity
- attr_reader :events
- attr_reader :status
- attr_reader :started_at
-
- def initialize(params)
- @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
- @project_url = params.dig(:project, :web_url) || params[:project_url]
- @title = params.dig(:object_attributes, :title)
- @alert_url = params.dig(:object_attributes, :url)
- @severity = params.dig(:object_attributes, :severity)
- @events = params.dig(:object_attributes, :events)
- @status = params.dig(:object_attributes, :status)
- @started_at = params.dig(:object_attributes, :started_at)
- end
-
- def attachments
- [{
- title: title,
- title_link: alert_url,
- color: attachment_color,
- fields: attachment_fields
- }]
- end
-
- def message
- "Alert firing in #{project_name}"
- end
-
- private
-
- def attachment_color
- "#C95823"
- end
-
- def attachment_fields
- [
- {
- title: "Severity",
- value: severity.to_s.humanize,
- short: true
- },
- {
- title: "Events",
- value: events,
- short: true
- },
- {
- title: "Status",
- value: status.to_s.humanize,
- short: true
- },
- {
- title: "Start time",
- value: format_time(started_at),
- short: true
- }
- ]
- end
-
- # This formats time into the following format
- # April 23rd, 2020 1:06AM UTC
- def format_time(time)
- time = Time.zone.parse(time.to_s)
- time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
- end
- end
-end
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
deleted file mode 100644
index bdd77a919e3..00000000000
--- a/app/models/project_services/chat_message/base_message.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class BaseMessage
- RELATIVE_LINK_REGEX = /!\[[^\]]*\]\((\/uploads\/[^\)]*)\)/.freeze
-
- attr_reader :markdown
- attr_reader :user_full_name
- attr_reader :user_name
- attr_reader :user_avatar
- attr_reader :project_name
- attr_reader :project_url
-
- def initialize(params)
- @markdown = params[:markdown] || false
- @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
- @project_url = params.dig(:project, :web_url) || params[:project_url]
- @user_full_name = params.dig(:user, :name) || params[:user_full_name]
- @user_name = params.dig(:user, :username) || params[:user_name]
- @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
- end
-
- def user_combined_name
- if user_full_name.present?
- "#{user_full_name} (#{user_name})"
- else
- user_name
- end
- end
-
- def summary
- return message if markdown
-
- format(message)
- end
-
- def pretext
- summary
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- raise NotImplementedError
- end
-
- def activity
- raise NotImplementedError
- end
-
- private
-
- def message
- raise NotImplementedError
- end
-
- def format(string)
- Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
- end
-
- def format_relative_links(string)
- string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
- end
-
- def attachment_color
- '#345'
- end
-
- def link(text, url)
- "[#{text}](#{url})"
- end
-
- def pretty_duration(seconds)
- parse_string =
- if duration < 1.hour
- '%M:%S'
- else
- '%H:%M:%S'
- end
-
- Time.at(seconds).utc.strftime(parse_string)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb
deleted file mode 100644
index 5deb757e60f..00000000000
--- a/app/models/project_services/chat_message/deployment_message.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class DeploymentMessage < BaseMessage
- attr_reader :commit_title
- attr_reader :commit_url
- attr_reader :deployable_id
- attr_reader :deployable_url
- attr_reader :environment
- attr_reader :short_sha
- attr_reader :status
- attr_reader :user_url
-
- def initialize(data)
- super
-
- @commit_title = data[:commit_title]
- @commit_url = data[:commit_url]
- @deployable_id = data[:deployable_id]
- @deployable_url = data[:deployable_url]
- @environment = data[:environment]
- @short_sha = data[:short_sha]
- @status = data[:status]
- @user_url = data[:user_url]
- end
-
- def attachments
- [{
- text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
- color: color
- }]
- end
-
- def activity
- {}
- end
-
- private
-
- def message
- if running?
- "Starting deploy to #{environment}"
- else
- "Deploy to #{environment} #{humanized_status}"
- end
- end
-
- def color
- case status
- when 'success'
- 'good'
- when 'canceled'
- 'warning'
- when 'failed'
- 'danger'
- else
- '#334455'
- end
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def deployment_link
- link("##{deployable_id}", deployable_url)
- end
-
- def user_link
- link(user_combined_name, user_url)
- end
-
- def commit_link
- link(short_sha, commit_url)
- end
-
- def humanized_status
- status == 'success' ? 'succeeded' : status
- end
-
- def running?
- status == 'running'
- end
- end
-end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
deleted file mode 100644
index c8e90b66bae..00000000000
--- a/app/models/project_services/chat_message/issue_message.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class IssueMessage < BaseMessage
- attr_reader :title
- attr_reader :issue_iid
- attr_reader :issue_url
- attr_reader :action
- attr_reader :state
- attr_reader :description
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @issue_iid = obj_attr[:iid]
- @issue_url = obj_attr[:url]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @description = obj_attr[:description] || ''
- end
-
- def attachments
- return [] unless opened_issue?
- return description if markdown
-
- description_message
- end
-
- def activity
- {
- title: "Issue #{state} by #{user_combined_name}",
- subtitle: "in #{project_link}",
- text: issue_link,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
- end
-
- def opened_issue?
- action == 'open'
- end
-
- def description_message
- [{
- title: issue_title,
- title_link: issue_url,
- text: format(description),
- color: '#C95823'
- }]
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def issue_link
- link(issue_title, issue_url)
- end
-
- def issue_title
- "#{Issue.reference_prefix}#{issue_iid} #{title}"
- end
- end
-end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
deleted file mode 100644
index e45bb9b8ce1..00000000000
--- a/app/models/project_services/chat_message/merge_message.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class MergeMessage < BaseMessage
- attr_reader :merge_request_iid
- attr_reader :source_branch
- attr_reader :target_branch
- attr_reader :action
- attr_reader :state
- attr_reader :title
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @merge_request_iid = obj_attr[:iid]
- @source_branch = obj_attr[:source_branch]
- @target_branch = obj_attr[:target_branch]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @title = format_title(obj_attr[:title])
- end
-
- def attachments
- []
- end
-
- def activity
- {
- title: "Merge request #{state_or_action_text} by #{user_combined_name}",
- subtitle: "in #{project_link}",
- text: merge_request_link,
- image: user_avatar
- }
- end
-
- private
-
- def format_title(title)
- '*' + title.lines.first.chomp + '*'
- end
-
- def message
- merge_request_message
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def merge_request_message
- "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
- end
-
- def merge_request_link
- link(merge_request_title, merge_request_url)
- end
-
- def merge_request_title
- "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
- end
-
- def merge_request_url
- "#{project_url}/-/merge_requests/#{merge_request_iid}"
- end
-
- def state_or_action_text
- case action
- when 'approved', 'unapproved'
- action
- when 'approval'
- 'added their approval to'
- when 'unapproval'
- 'removed their approval from'
- else
- state
- end
- end
- end
-end
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
deleted file mode 100644
index 741474fb27b..00000000000
--- a/app/models/project_services/chat_message/note_message.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class NoteMessage < BaseMessage
- attr_reader :note
- attr_reader :note_url
- attr_reader :title
- attr_reader :target
-
- def initialize(params)
- super
-
- params = HashWithIndifferentAccess.new(params)
- obj_attr = params[:object_attributes]
- @note = obj_attr[:note]
- @note_url = obj_attr[:url]
- @target, @title = case obj_attr[:noteable_type]
- when "Commit"
- create_commit_note(params[:commit])
- when "Issue"
- create_issue_note(params[:issue])
- when "MergeRequest"
- create_merge_note(params[:merge_request])
- when "Snippet"
- create_snippet_note(params[:snippet])
- end
- end
-
- def attachments
- return note if markdown
-
- description_message
- end
-
- def activity
- {
- title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
- subtitle: "in #{project_link}",
- text: formatted_title,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
- end
-
- def format_title(title)
- title.lines.first.chomp
- end
-
- def formatted_title
- format_title(title)
- end
-
- def create_issue_note(issue)
- ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
- end
-
- def create_commit_note(commit)
- commit_sha = Commit.truncate_sha(commit[:id])
-
- ["commit #{commit_sha}", commit[:message]]
- end
-
- def create_merge_note(merge_request)
- ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
- end
-
- def create_snippet_note(snippet)
- ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
- end
-
- def description_message
- [{ text: format(note), color: attachment_color }]
- end
-
- def project_link
- link(project_name, project_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
deleted file mode 100644
index f4c6938fa78..00000000000
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ /dev/null
@@ -1,265 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class PipelineMessage < BaseMessage
- MAX_VISIBLE_JOBS = 10
-
- attr_reader :user
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :detailed_status
- attr_reader :duration
- attr_reader :finished_at
- attr_reader :pipeline_id
- attr_reader :failed_stages
- attr_reader :failed_jobs
-
- attr_reader :project
- attr_reader :commit
- attr_reader :committer
- attr_reader :pipeline
-
- def initialize(data)
- super
-
- @user = data[:user]
- @user_name = data.dig(:user, :username) || 'API'
-
- pipeline_attributes = data[:object_attributes]
- @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
- @ref = pipeline_attributes[:ref]
- @status = pipeline_attributes[:status]
- @detailed_status = pipeline_attributes[:detailed_status]
- @duration = pipeline_attributes[:duration].to_i
- @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
- @pipeline_id = pipeline_attributes[:id]
-
- # Get list of jobs that have actually failed (after exhausting all retries)
- @failed_jobs = actually_failed_jobs(Array(data[:builds]))
- @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
-
- @project = Project.find(data[:project][:id])
- @commit = project.commit_by(oid: data[:commit][:id])
- @committer = commit.committer
- @pipeline = Ci::Pipeline.find(pipeline_id)
- end
-
- def pretext
- ''
- end
-
- def attachments
- return message if markdown
-
- [{
- fallback: format(message),
- color: attachment_color,
- author_name: user_combined_name,
- author_icon: user_avatar,
- author_link: author_url,
- title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
- {
- pipeline_id: pipeline_id,
- humanized_status: humanized_status,
- duration: pretty_duration(duration)
- },
- title_link: pipeline_url,
- fields: attachments_fields,
- footer: project.name,
- footer_icon: project.avatar_url(only_path: false),
- ts: finished_at
- }]
- end
-
- def activity
- {
- title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
- {
- pipeline_link: pipeline_link,
- ref_type: ref_type,
- ref_link: ref_link,
- user_combined_name: user_combined_name,
- humanized_status: humanized_status
- },
- subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
- text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
- image: user_avatar || ''
- }
- end
-
- private
-
- def actually_failed_jobs(builds)
- succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
-
- failed_jobs = builds.select do |build|
- # Select jobs which doesn't have a successful retry
- build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
- end
-
- failed_jobs.uniq { |job| job[:name] }.reverse
- end
-
- def failed_stages_field
- {
- title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
- short: true
- }
- end
-
- def failed_jobs_field
- {
- title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
- short: true
- }
- end
-
- def yaml_error_field
- {
- title: s_("ChatMessage|Invalid CI config YAML file"),
- value: pipeline.yaml_errors,
- short: false
- }
- end
-
- def attachments_fields
- fields = [
- {
- title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
- value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
- short: true
- },
- {
- title: s_("ChatMessage|Commit"),
- value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
- short: true
- }
- ]
-
- fields << failed_stages_field if failed_stages.any?
- fields << failed_jobs_field if failed_jobs.any?
- fields << yaml_error_field if pipeline.has_yaml_errors?
-
- fields
- end
-
- def message
- s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
- {
- project_link: project_link,
- pipeline_link: pipeline_link,
- ref_type: ref_type,
- ref_link: ref_link,
- user_combined_name: user_combined_name,
- humanized_status: humanized_status,
- duration: pretty_duration(duration)
- }
- end
-
- def humanized_status
- case status
- when 'success'
- detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
- when 'failed'
- s_("ChatMessage|has failed")
- else
- status
- end
- end
-
- def attachment_color
- case status
- when 'success'
- detailed_status == 'passed with warnings' ? 'warning' : 'good'
- else
- 'danger'
- end
- end
-
- def ref_url
- if ref_type == 'tag'
- "#{project_url}/-/tags/#{ref}"
- else
- "#{project_url}/-/commits/#{ref}"
- end
- end
-
- def ref_link
- "[#{ref}](#{ref_url})"
- end
-
- def project_url
- project.web_url
- end
-
- def project_link
- "[#{project.name}](#{project_url})"
- end
-
- def pipeline_failed_jobs_url
- "#{project_url}/-/pipelines/#{pipeline_id}/failures"
- end
-
- def pipeline_url
- if failed_jobs.any?
- pipeline_failed_jobs_url
- else
- "#{project_url}/-/pipelines/#{pipeline_id}"
- end
- end
-
- def pipeline_link
- "[##{pipeline_id}](#{pipeline_url})"
- end
-
- def job_url(job)
- "#{project_url}/-/jobs/#{job[:id]}"
- end
-
- def job_link(job)
- "[#{job[:name]}](#{job_url(job)})"
- end
-
- def failed_jobs_links
- failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
- truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
-
- failed_links = failed.map { |job| job_link(job) }
-
- unless truncated.blank?
- failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
- count: truncated.size,
- pipeline_failed_jobs_url: pipeline_failed_jobs_url
- }
- end
-
- failed_links.join(I18n.translate(:'support.array.words_connector'))
- end
-
- def stage_link(stage)
- # All stages link to the pipeline page
- "[#{stage}](#{pipeline_url})"
- end
-
- def failed_stages_links
- failed_stages.map { |s| stage_link(s) }.join(I18n.translate(:'support.array.words_connector'))
- end
-
- def commit_url
- Gitlab::UrlBuilder.build(commit)
- end
-
- def commit_link
- "[#{commit.title}](#{commit_url})"
- end
-
- def author_url
- return unless user && committer
-
- Gitlab::UrlBuilder.build(committer)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
deleted file mode 100644
index c8e70a69c88..00000000000
--- a/app/models/project_services/chat_message/push_message.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class PushMessage < BaseMessage
- attr_reader :after
- attr_reader :before
- attr_reader :commits
- attr_reader :ref
- attr_reader :ref_type
-
- def initialize(params)
- super
-
- @after = params[:after]
- @before = params[:before]
- @commits = params.fetch(:commits, [])
- @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
- @ref = Gitlab::Git.ref_name(params[:ref])
- end
-
- def attachments
- return [] if new_branch? || removed_branch?
- return commit_messages if markdown
-
- commit_message_attachments
- end
-
- def activity
- {
- title: humanized_action(short: true),
- subtitle: "in #{project_link}",
- text: compare_link,
- image: user_avatar
- }
- end
-
- private
-
- def humanized_action(short: false)
- action, ref_link, target_link = compose_action_details
- text = [user_combined_name, action, ref_type, ref_link]
- text << target_link unless short
- text.join(' ')
- end
-
- def message
- humanized_action
- end
-
- def format(string)
- Slack::Messenger::Util::LinkFormatter.format(string)
- end
-
- def commit_messages
- commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
- end
-
- def commit_message_attachments
- [{ text: format(commit_messages), color: attachment_color }]
- end
-
- def compose_commit_message(commit)
- author = commit[:author][:name]
- id = Commit.truncate_sha(commit[:id])
- title = commit[:title]
-
- url = commit[:url]
-
- "[#{id}](#{url}): #{title} - #{author}"
- end
-
- def new_branch?
- Gitlab::Git.blank_ref?(before)
- end
-
- def removed_branch?
- Gitlab::Git.blank_ref?(after)
- end
-
- def ref_url
- if ref_type == 'tag'
- "#{project_url}/-/tags/#{ref}"
- else
- "#{project_url}/commits/#{ref}"
- end
- end
-
- def compare_url
- "#{project_url}/compare/#{before}...#{after}"
- end
-
- def ref_link
- "[#{ref}](#{ref_url})"
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def compare_link
- "[Compare changes](#{compare_url})"
- end
-
- def compose_action_details
- if new_branch?
- ['pushed new', ref_link, "to #{project_link}"]
- elsif removed_branch?
- ['removed', ref, "from #{project_link}"]
- else
- ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
- end
- end
-
- def attachment_color
- '#345'
- end
- end
-end
diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
deleted file mode 100644
index ebe7abb379f..00000000000
--- a/app/models/project_services/chat_message/wiki_page_message.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class WikiPageMessage < BaseMessage
- attr_reader :title
- attr_reader :wiki_page_url
- attr_reader :action
- attr_reader :description
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @wiki_page_url = obj_attr[:url]
- @description = obj_attr[:message]
-
- @action =
- case obj_attr[:action]
- when "create"
- "created"
- when "update"
- "edited"
- end
- end
-
- def attachments
- return description if markdown
-
- description_message
- end
-
- def activity
- {
- title: "#{user_combined_name} #{action} #{wiki_page_link}",
- subtitle: "in #{project_link}",
- text: title,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
- end
-
- def description_message
- [{ text: format(@description), color: attachment_color }]
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def wiki_page_link
- "[wiki page](#{wiki_page_url})"
- end
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 4a99842b4d5..2f841bf903e 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -2,7 +2,7 @@
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
-class ChatNotificationService < Service
+class ChatNotificationService < Integration
include ChatMessage
include NotificationBranchSelection
@@ -15,9 +15,14 @@ class ChatNotificationService < Service
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
+ LABEL_NOTIFICATION_BEHAVIOURS = [
+ MATCH_ANY_LABEL = 'match_any',
+ MATCH_ALL_LABELS = 'match_all'
+ ].freeze
+
default_value_for :category, 'chat'
- prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified
+ prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
@@ -25,12 +30,14 @@ class ChatNotificationService < Service
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, public_url: true, if: :activated?
+ validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
if properties.nil?
self.properties = {}
self.notify_only_broken_pipelines = true
self.branches_to_be_notified = "default"
+ self.labels_to_be_notified_behavior = MATCH_ANY_LABEL
elsif !self.notify_only_default_branch.nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
@@ -65,7 +72,20 @@ class ChatNotificationService < Service
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
- { type: 'text', name: 'labels_to_be_notified', placeholder: '~backend,~frontend', help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' }.freeze
+ {
+ type: 'text',
+ name: 'labels_to_be_notified',
+ placeholder: '~backend,~frontend',
+ help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
+ }.freeze,
+ {
+ type: 'select',
+ name: 'labels_to_be_notified_behavior',
+ choices: [
+ ['Match any of the labels', MATCH_ANY_LABEL],
+ ['Match all of the labels', MATCH_ALL_LABELS]
+ ]
+ }.freeze
].freeze
end
@@ -136,11 +156,17 @@ class ChatNotificationService < Service
def notify_label?(data)
return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
- issue_labels = data.dig(:issue, :labels) || []
- merge_request_labels = data.dig(:merge_request, :labels) || []
- label_titles = (issue_labels + merge_request_labels).pluck(:title)
+ labels = data.dig(:issue, :labels) || data.dig(:merge_request, :labels)
+
+ return false if labels.nil?
- (labels_to_be_notified_list & label_titles).any?
+ matching_labels = labels_to_be_notified_list & labels.pluck(:title)
+
+ if labels_to_be_notified_behavior == MATCH_ALL_LABELS
+ labels_to_be_notified_list.difference(matching_labels).empty?
+ else
+ matching_labels.any?
+ end
end
def user_id_from_hook_data(data)
@@ -159,19 +185,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
+ Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
- ChatMessage::IssueMessage.new(data) unless update?(data)
+ Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
- ChatMessage::MergeMessage.new(data) unless update?(data)
+ Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
- ChatMessage::NoteMessage.new(data)
+ Integrations::ChatMessage::NoteMessage.new(data)
when "pipeline"
- ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
- ChatMessage::WikiPageMessage.new(data)
+ Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
- ChatMessage::DeploymentMessage.new(data)
+ Integrations::ChatMessage::DeploymentMessage.new(data)
end
end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 29edb9ec16f..0733da761d5 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -3,7 +3,7 @@
# Base class for CI services
# List methods you need to implement to get your CI service
# working with GitLab merge requests
-class CiService < Service
+class CiService < Integration
default_value_for :category, 'ci'
def valid_token?(token)
diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb
deleted file mode 100644
index 8a6f4de540c..00000000000
--- a/app/models/project_services/confluence_service.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-class ConfluenceService < Service
- include ActionView::Helpers::UrlHelper
-
- VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze
- VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
- VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
-
- prop_accessor :confluence_url
-
- validates :confluence_url, presence: true, if: :activated?
- validate :validate_confluence_url_is_cloud, if: :activated?
-
- after_commit :cache_project_has_confluence
-
- def self.to_param
- 'confluence'
- end
-
- def self.supported_events
- %w()
- end
-
- def title
- s_('ConfluenceService|Confluence Workspace')
- end
-
- def description
- s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
- end
-
- def help
- return unless project&.wiki_enabled?
-
- if activated?
- wiki_url = project.wiki.web_url
-
- s_(
- 'ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration' %
- { wiki_link: link_to(wiki_url, wiki_url) }
- ).html_safe
- else
- s_('ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration').html_safe
- end
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'confluence_url',
- title: 'Confluence Cloud Workspace URL',
- placeholder: s_('ConfluenceService|The URL of the Confluence Workspace'),
- required: true
- }
- ]
- end
-
- def can_test?
- false
- end
-
- private
-
- def validate_confluence_url_is_cloud
- unless confluence_uri_valid?
- errors.add(:confluence_url, 'URL must be to a Confluence Cloud Workspace hosted on atlassian.net')
- end
- end
-
- def confluence_uri_valid?
- return false unless confluence_url
-
- uri = URI.parse(confluence_url)
-
- (uri.scheme&.match(VALID_SCHEME_MATCH) &&
- uri.host&.match(VALID_HOST_MATCH) &&
- uri.path&.match(VALID_PATH_MATCH)).present?
-
- rescue URI::InvalidURIError
- false
- end
-
- def cache_project_has_confluence
- return unless project && !project.destroyed?
-
- project.project_setting.save! unless project.project_setting.persisted?
- project.project_setting.update_column(:has_confluence, active?)
- end
-end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index aab8661ec55..6f99d104904 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -1,25 +1,23 @@
# frozen_string_literal: true
class CustomIssueTrackerService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
def title
- 'Custom Issue Tracker'
+ s_('IssueTracker|Custom issue tracker')
end
def description
- s_('IssueTracker|Custom issue tracker')
+ s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
end
- def self.to_param
- 'custom_issue_tracker'
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer'
+ s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
- def fields
- [
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
- { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
- ]
+ def self.to_param
+ 'custom_issue_tracker'
end
end
diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb
index 12ebf260e08..ca4dc0375fb 100644
--- a/app/models/project_services/data_fields.rb
+++ b/app/models/project_services/data_fields.rb
@@ -42,9 +42,9 @@ module DataFields
end
included do
- has_one :issue_tracker_data, autosave: true
- has_one :jira_tracker_data, autosave: true
- has_one :open_project_tracker_data, autosave: true
+ has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id
+ has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id
+ has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id
def data_fields
raise NotImplementedError
diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb
deleted file mode 100644
index 9a2d99c46c9..00000000000
--- a/app/models/project_services/datadog_service.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-# frozen_string_literal: true
-
-class DatadogService < Service
- DEFAULT_SITE = 'datadoghq.com'
- URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'
- URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'
- URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/"
-
- SUPPORTED_EVENTS = %w[
- pipeline job
- ].freeze
-
- prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env
-
- with_options if: :activated? do
- validates :api_key, presence: true, format: { with: /\A\w+\z/ }
- validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
- validates :api_url, public_url: { allow_blank: true }
- validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
- validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
- end
-
- after_save :compose_service_hook, if: :activated?
-
- def initialize_properties
- super
-
- self.datadog_site ||= DEFAULT_SITE
- end
-
- def self.supported_events
- SUPPORTED_EVENTS
- end
-
- def self.default_test_event
- 'pipeline'
- end
-
- def configurable_events
- [] # do not allow to opt out of required hooks
- end
-
- def title
- 'Datadog'
- end
-
- def description
- 'Trace your GitLab pipelines with Datadog'
- end
-
- def help
- nil
- # Maybe adding something in the future
- # We could link to static help pages as well
- # [More information](#{Gitlab::Routing.url_helpers.help_page_url('integration/datadog')})"
- end
-
- def self.to_param
- 'datadog'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'datadog_site',
- placeholder: DEFAULT_SITE,
- help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
- required: false
- },
- {
- type: 'text',
- name: 'api_url',
- title: 'API URL',
- help: '(Advanced) Define the full URL for your Datadog site directly',
- required: false
- },
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
- help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
- required: true
- },
- {
- type: 'text',
- name: 'datadog_service',
- title: 'Service',
- placeholder: 'gitlab-ci',
- help: 'Name of this GitLab instance that all data will be tagged with'
- },
- {
- type: 'text',
- name: 'datadog_env',
- title: 'Env',
- help: 'The environment tag that traces will be tagged with'
- }
- ]
- end
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.url = hook_url
- hook.save
- end
-
- def hook_url
- url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site)
- url = URI.parse(url)
- url.path = File.join(url.path || '/', api_key)
- query = { service: datadog_service.presence, env: datadog_env.presence }.compact
- url.query = query.to_query unless query.empty?
- url.to_s
- end
-
- def api_keys_url
- return URL_API_KEYS_DOCS unless datadog_site.presence
-
- sprintf(URL_TEMPLATE_API_KEYS, datadog_site: datadog_site)
- end
-
- def execute(data)
- return if project.disabled_services.include?(to_param)
-
- object_kind = data[:object_kind]
- object_kind = 'job' if object_kind == 'build'
- return unless supported_events.include?(object_kind)
-
- service_hook.execute(data, "#{object_kind} hook")
- end
-
- def test(data)
- begin
- result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 200
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result[:message] }
- end
-end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
deleted file mode 100644
index cdb69684d16..00000000000
--- a/app/models/project_services/emails_on_push_service.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-class EmailsOnPushService < Service
- include NotificationBranchSelection
-
- RECIPIENTS_LIMIT = 750
-
- boolean_accessor :send_from_committer_email
- boolean_accessor :disable_diffs
- prop_accessor :recipients, :branches_to_be_notified
- validates :recipients, presence: true, if: :validate_recipients?
- validate :number_of_recipients_within_limit, if: :validate_recipients?
-
- def self.valid_recipients(recipients)
- recipients.split.select do |recipient|
- recipient.include?('@')
- end.uniq(&:downcase)
- end
-
- def title
- s_('EmailsOnPushService|Emails on push')
- end
-
- def description
- s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
- end
-
- def self.to_param
- 'emails_on_push'
- end
-
- def self.supported_events
- %w(push tag_push)
- end
-
- def initialize_properties
- super
-
- self.branches_to_be_notified = 'all' if branches_to_be_notified.nil?
- end
-
- def execute(push_data)
- return unless supported_events.include?(push_data[:object_kind])
- return if project.emails_disabled?
- return unless notify_for_ref?(push_data)
-
- EmailsOnPushWorker.perform_async(
- project_id,
- recipients,
- push_data,
- send_from_committer_email: send_from_committer_email?,
- disable_diffs: disable_diffs?
- )
- end
-
- def notify_for_ref?(push_data)
- return true if push_data[:object_kind] == 'tag_push'
- return true if push_data.dig(:object_attributes, :tag)
-
- notify_for_branch?(push_data)
- end
-
- def send_from_committer_email?
- Gitlab::Utils.to_boolean(self.send_from_committer_email)
- end
-
- def disable_diffs?
- Gitlab::Utils.to_boolean(self.disable_diffs)
- end
-
- def fields
- domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
- [
- { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"),
- help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
- { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
- help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
- {
- type: 'textarea',
- name: 'recipients',
- placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'),
- help: s_('EmailsOnPushService|Emails separated by whitespace.')
- }
- ]
- end
-
- private
-
- def number_of_recipients_within_limit
- return if recipients.blank?
-
- if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
- errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
- end
- end
-end
diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb
index af402e50292..90fcbb10d2b 100644
--- a/app/models/project_services/ewm_service.rb
+++ b/app/models/project_services/ewm_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class EwmService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
def self.reference_pattern(only_long: true)
@@ -12,7 +14,12 @@ class EwmService < IssueTrackerService
end
def description
- s_('IssueTracker|EWM work items tracker')
+ s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index c41783d1af4..f49b008533d 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-class ExternalWikiService < Service
+class ExternalWikiService < Integration
include ActionView::Helpers::UrlHelper
+
prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, public_url: true, if: :activated?
@@ -39,7 +40,7 @@ class ExternalWikiService < Service
def execute(_data)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response.body if response.code == 200
- rescue
+ rescue StandardError
nil
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index e721fded1d9..7aae5af7454 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-class FlowdockService < Service
+class FlowdockService < Integration
+ include ActionView::Helpers::UrlHelper
+
prop_accessor :token
validates :token, presence: true, if: :activated?
@@ -9,7 +11,12 @@ class FlowdockService < Service
end
def description
- s_('FlowdockService|Flowdock is a collaboration web app for technical teams.')
+ s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer'
+ s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -18,7 +25,7 @@ class FlowdockService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: s_('FlowdockService|Flowdock Git source token'), required: true }
+ { type: 'text', name: 'token', placeholder: s_('FlowdockService|1b609b52537...'), required: true, help: 'Enter your Flowdock token.' }
]
end
diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb
index 299a306add7..6e7708a169f 100644
--- a/app/models/project_services/hangouts_chat_service.rb
+++ b/app/models/project_services/hangouts_chat_service.rb
@@ -3,12 +3,14 @@
require 'hangouts_chat'
class HangoutsChatService < ChatNotificationService
+ include ActionView::Helpers::UrlHelper
+
def title
- 'Hangouts Chat'
+ 'Google Chat'
end
def description
- 'Receive event notifications in Google Hangouts Chat'
+ 'Send notifications from GitLab to a room in Google Chat.'
end
def self.to_param
@@ -16,13 +18,8 @@ class HangoutsChatService < ChatNotificationService
end
def help
- 'This service sends notifications about projects events to Google Hangouts Chat room.<br />
- To set up this service:
- <ol>
- <li><a href="https://developers.google.com/hangouts/chat/how-tos/webhooks">Set up an incoming webhook for your room</a>. All notifications will come to this room.</li>
- <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
- <li>Select events below to enable notifications.</li>
- </ol>'
+ docs_link = link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
@@ -42,7 +39,7 @@ class HangoutsChatService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
+ { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index cd49c6d253d..71d8e7bfac4 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -1,54 +1,17 @@
# frozen_string_literal: true
-class HipchatService < Service
- include ActionView::Helpers::SanitizeHelper
-
- MAX_COMMITS = 3
- HIPCHAT_ALLOWED_TAGS = %w[
- a b i strong em br img pre code
- table th tr td caption colgroup col thead tbody tfoot
- ul ol li dl dt dd
- ].freeze
-
- prop_accessor :token, :room, :server, :color, :api_version
- boolean_accessor :notify_only_broken_pipelines, :notify
- validates :token, presence: true, if: :activated?
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_pipelines = true
- end
- end
-
- def title
- 'HipChat'
- end
-
- def description
- 'Private group chat and IM'
- end
+# This service is scheduled for removal. All records must
+# be deleted before the class can be removed.
+# https://gitlab.com/gitlab-org/gitlab/-/issues/27954
+class HipchatService < Integration
+ before_save :prevent_save
def self.to_param
'hipchat'
end
- def fields
- [
- { type: 'text', name: 'token', placeholder: 'Room token', required: true },
- { type: 'text', name: 'room', placeholder: 'Room name or ID' },
- { type: 'checkbox', name: 'notify' },
- { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
- { type: 'text', name: 'api_version', title: _('API version'),
- placeholder: 'Leave blank for default (v2)' },
- { type: 'text', name: 'server',
- placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' }
- ]
- end
-
def self.supported_events
- %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
+ []
end
def execute(data)
@@ -56,96 +19,14 @@ class HipchatService < Service
# HipChat is unusable anyway, so do nothing in this method
end
- def test(data)
- begin
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
private
- def message_options(data = nil)
- { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
- end
-
- def render_line(text)
- markdown(text.lines.first.chomp, pipeline: :single_line) if text
- end
-
- def markdown(text, options = {})
- return "" unless text
-
- context = {
- project: project,
- pipeline: :email
- }
-
- Banzai.render(text, context)
-
- context.merge!(options)
-
- html = Banzai.render_and_post_process(text, context)
- sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
-
- sanitized_html.truncate(200, separator: ' ', omission: '...')
- end
-
- def format_title(title)
- "<b>#{render_line(title)}</b>"
- end
-
- def message_color(data)
- pipeline_status_color(data) || color || 'yellow'
- end
-
- def pipeline_status_color(data)
- return unless data && data[:object_kind] == 'pipeline'
-
- case data[:object_attributes][:status]
- when 'success'
- 'green'
- else
- 'red'
- end
- end
-
- def project_name
- project.full_name.gsub(/\s/, '')
- end
-
- def project_url
- project.web_url
- end
-
- def project_link
- "<a href=\"#{project_url}\">#{project_name}</a>"
- end
-
- def update?(data)
- data[:object_attributes][:action] == 'update'
- end
-
- def humanized_status(status)
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
+ def prevent_save
+ errors.add(:base, _('HipChat endpoint is deprecated and should not be created or modified.'))
- def should_pipeline_be_notified?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
+ # Stops execution of callbacks and database operation while
+ # preserving expectations of #save (will not raise) & #save! (raises)
+ # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution
+ throw :abort # rubocop:disable Cop/BanCatchThrow
end
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 4f1ce16ebb2..5cca620c659 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -2,7 +2,7 @@
require 'uri'
-class IrkerService < Service
+class IrkerService < Integration
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
@@ -15,8 +15,7 @@ class IrkerService < Service
end
def description
- 'Send IRC messages, on update, to a list of recipients through an Irker '\
- 'gateway.'
+ 'Send IRC messages.'
end
def self.to_param
@@ -103,7 +102,7 @@ class IrkerService < Service
begin
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
- rescue
+ rescue StandardError
log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
end
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 19a5b4a74bb..099e3c336dd 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class IssueTrackerService < Service
+class IssueTrackerService < Integration
validate :one_issue_tracker, if: :activated?, on: :manual_change
# TODO: we can probably just delegate as part of
@@ -73,9 +73,9 @@ class IssueTrackerService < Service
def fields
[
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
- { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in the external issue tracker.'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('IssueTracker|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('IssueTracker|New issue URL'), help: s_('IssueTracker|The URL to create an issue in the external issue tracker.'), required: true }
]
end
@@ -143,10 +143,10 @@ class IssueTrackerService < Service
return if template? || instance?
return if project.blank?
- if project.services.external_issue_trackers.where.not(id: id).any?
+ if project.integrations.external_issue_trackers.where.not(id: id).any?
errors.add(:base, _('Another issue tracker is already in use. Only one issue tracker service can be active at a time'))
end
end
end
-IssueTrackerService.prepend_if_ee('EE::IssueTrackerService')
+IssueTrackerService.prepend_mod_with('IssueTrackerService')
diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb
index 6a123517b84..990a35cd617 100644
--- a/app/models/project_services/jenkins_service.rb
+++ b/app/models/project_services/jenkins_service.rb
@@ -64,12 +64,12 @@ class JenkinsService < CiService
end
def description
- s_('An extendable open source CI/CD server.')
+ s_('Run CI/CD pipelines with Jenkins.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
- s_('Trigger Jenkins builds when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 3e14bf44c12..5cd6e79eb1d 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -106,9 +106,8 @@ class JiraService < IssueTrackerService
end
def help
- "You need to configure Jira before enabling this service. For more details
- read the
- [Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
+ jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') }
+ s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
end
def title
@@ -116,7 +115,7 @@ class JiraService < IssueTrackerService
end
def description
- s_('JiraService|Track issues in Jira')
+ s_("JiraService|Use Jira as this project's issue tracker.")
end
def self.to_param
@@ -305,7 +304,7 @@ class JiraService < IssueTrackerService
)
true
- rescue => error
+ rescue StandardError => error
log_error(
"Issue transition failed",
error: {
@@ -490,7 +489,7 @@ class JiraService < IssueTrackerService
# Handle errors when doing Jira API calls
def jira_request
yield
- rescue => error
+ rescue StandardError => error
@error = error
log_error("Error sending message", client_url: client_url, error: @error.message)
nil
@@ -539,4 +538,4 @@ class JiraService < IssueTrackerService
end
end
-JiraService.prepend_if_ee('EE::JiraService')
+JiraService.prepend_mod_with('JiraService')
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index 803c1255195..1d2067067da 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -6,7 +6,7 @@ class MicrosoftTeamsService < ChatNotificationService
end
def description
- 'Receive event notifications in Microsoft Teams'
+ 'Send notifications about project events to Microsoft Teams.'
end
def self.to_param
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
index 1b530a8247b..ea65a200027 100644
--- a/app/models/project_services/monitoring_service.rb
+++ b/app/models/project_services/monitoring_service.rb
@@ -4,7 +4,7 @@
#
# These services integrate with a deployment solution like Prometheus
# to provide additional features for environments.
-class MonitoringService < Service
+class MonitoringService < Integration
default_value_for :category, 'monitoring'
def self.supported_events
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
index 21f0a2b2463..f3ea8c64302 100644
--- a/app/models/project_services/packagist_service.rb
+++ b/app/models/project_services/packagist_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PackagistService < Service
+class PackagistService < Integration
prop_accessor :username, :token, :server
validates :username, presence: true, if: :activated?
@@ -16,7 +16,7 @@ class PackagistService < Service
end
def description
- s_('Integrations|Update your projects on Packagist, the main Composer repository')
+ s_('Integrations|Update your Packagist projects.')
end
def self.to_param
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 0a0a41c525c..4603193ac8e 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PipelinesEmailService < Service
+class PipelinesEmailService < Integration
include NotificationBranchSelection
prop_accessor :recipients, :branches_to_be_notified
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index d3fff100964..6e67984591d 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PivotaltrackerService < Service
+class PivotaltrackerService < Integration
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
prop_accessor :token, :restrict_to_branch
@@ -11,7 +11,7 @@ class PivotaltrackerService < Service
end
def description
- s_('PivotalTrackerService|Project Management Software (Source Commits Endpoint)')
+ s_('PivotalTrackerService|Add commit messages as comments to PivotalTracker stories.')
end
def self.to_param
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 1781ec7456d..89765fbdf41 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PushoverService < Service
+class PushoverService < Integration
BASE_URI = 'https://api.pushover.net/1'
prop_accessor :api_key, :user_key, :device, :priority, :sound
@@ -11,7 +11,7 @@ class PushoverService < Service
end
def description
- s_('PushoverService|Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.')
+ s_('PushoverService|Get real-time notifications on your device.')
end
def self.to_param
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index 26a6cf86bf4..7a0f500209c 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -9,7 +9,7 @@ class RedmineService < IssueTrackerService
end
def description
- s_('IssueTracker|Use Redmine as the issue tracker.')
+ s_("IssueTracker|Use Redmine as this project's issue tracker.")
end
def help
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 7badcc24870..92a46f8d01f 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -39,7 +39,7 @@ class SlackService < ChatNotificationService
end
def get_message(object_kind, data)
- return ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+ return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super
end
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
index d436176a52c..37d16737052 100644
--- a/app/models/project_services/slash_commands_service.rb
+++ b/app/models/project_services/slash_commands_service.rb
@@ -2,7 +2,7 @@
# Base class for Chat services
# This class is not meant to be used directly, but only to inherrit from.
-class SlashCommandsService < Service
+class SlashCommandsService < Integration
default_value_for :category, 'chat'
prop_accessor :token
diff --git a/app/models/project_services/unify_circuit_service.rb b/app/models/project_services/unify_circuit_service.rb
index 1a0eebe7d64..5f43388e1c9 100644
--- a/app/models/project_services/unify_circuit_service.rb
+++ b/app/models/project_services/unify_circuit_service.rb
@@ -6,7 +6,7 @@ class UnifyCircuitService < ChatNotificationService
end
def description
- 'Receive event notifications in Unify Circuit'
+ s_('Integrations|Send notifications about project events to Unify Circuit.')
end
def self.to_param
diff --git a/app/models/project_services/webex_teams_service.rb b/app/models/project_services/webex_teams_service.rb
index 4e8281f4e81..3d92d3bb85e 100644
--- a/app/models/project_services/webex_teams_service.rb
+++ b/app/models/project_services/webex_teams_service.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
class WebexTeamsService < ChatNotificationService
+ include ActionView::Helpers::UrlHelper
+
def title
- 'Webex Teams'
+ s_("WebexTeamsService|Webex Teams")
end
def description
- 'Receive event notifications in Webex Teams'
+ s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
def self.to_param
@@ -14,13 +16,8 @@ class WebexTeamsService < ChatNotificationService
end
def help
- 'This service sends notifications about projects events to a Webex Teams conversation.<br />
- To set up this service:
- <ol>
- <li><a href="https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
- <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
- <li>Select events below to enable notifications.</li>
- </ol>'
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
+ s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
def event_field(event)
@@ -36,7 +33,7 @@ class WebexTeamsService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. https://api.ciscospark.com/v1/webhooks/incoming/…", required: true },
+ { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 30abd0159b3..9760a22a872 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class YoutrackService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
@@ -17,7 +19,12 @@ class YoutrackService < IssueTrackerService
end
def description
- s_('IssueTracker|YouTrack issue tracker')
+ s_("IssueTracker|Use YouTrack as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -26,8 +33,8 @@ class YoutrackService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }
]
end
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 83ff0702b88..24d892290a6 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -21,4 +21,4 @@ class ProjectSetting < ApplicationRecord
end
end
-ProjectSetting.prepend_ee_mod
+ProjectSetting.prepend_mod
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 8c3dcaa7c0f..37ddd2d030d 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -159,4 +159,4 @@ class ProjectStatistics < ApplicationRecord
end
end
-ProjectStatistics.prepend_if_ee('EE::ProjectStatistics')
+ProjectStatistics.prepend_mod_with('ProjectStatistics')
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 1a3f362e6a1..a85afada901 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -130,7 +130,7 @@ class ProjectTeam
end
true
- rescue
+ rescue StandardError
false
end
@@ -234,4 +234,4 @@ class ProjectTeam
end
end
-ProjectTeam.prepend_if_ee('EE::ProjectTeam')
+ProjectTeam.prepend_mod_with('ProjectTeam')
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 91fb3d4e4ba..ffffa803011 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -32,4 +32,4 @@ end
# TODO: Remove this once we implement ES support for group wikis.
# https://gitlab.com/gitlab-org/gitlab/-/issues/207889
-ProjectWiki.prepend_if_ee('EE::ProjectWiki')
+ProjectWiki.prepend_mod_with('ProjectWiki')
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 963a6b7774a..889eaed138d 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -63,4 +63,4 @@ class ProtectedBranch < ApplicationRecord
end
end
-ProtectedBranch.prepend_if_ee('EE::ProtectedBranch')
+ProtectedBranch.prepend_mod_with('ProtectedBranch')
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
index 2786ecb641a..8358be35470 100644
--- a/app/models/push_event_payload.rb
+++ b/app/models/push_event_payload.rb
@@ -25,4 +25,4 @@ class PushEventPayload < ApplicationRecord
}
end
-PushEventPayload.prepend_if_ee('EE::PushEventPayload')
+PushEventPayload.prepend_mod_with('PushEventPayload')
diff --git a/app/models/release.rb b/app/models/release.rb
index 5ca8f537baa..1889a0707b4 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -13,6 +13,7 @@ class Release < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :links, class_name: 'Releases::Link'
+ has_many :sorted_links, -> { sorted }, class_name: 'Releases::Link', inverse_of: :release
has_many :milestone_releases
has_many :milestones, through: :milestone_releases
@@ -23,11 +24,15 @@ class Release < ApplicationRecord
before_create :set_released_at
validates :project, :tag, presence: true
+ validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed?
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
scope :sorted, -> { order(released_at: :desc) }
- scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }
+ scope :preloaded, -> {
+ includes(:author, :evidences, :milestones, :links, :sorted_links,
+ project: [:project_feature, :route, { namespace: :route }])
+ }
scope :with_project_and_namespace, -> { includes(project: :namespace) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) }
@@ -58,8 +63,8 @@ class Release < ApplicationRecord
end
def assets_count(except: [])
- links_count = links.count
- sources_count = except.include?(:sources) ? 0 : sources.count
+ links_count = links.size
+ sources_count = except.include?(:sources) ? 0 : sources.size
links_count + sources_count
end
@@ -123,4 +128,4 @@ class Release < ApplicationRecord
end
end
-Release.prepend_if_ee('EE::Release')
+Release.prepend_mod_with('Release')
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 98d9899a349..9c30d0611e6 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -4,6 +4,10 @@ class ReleaseHighlight
CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
+ FREE_PACKAGE = 'Free'
+ PREMIUM_PACKAGE = 'Premium'
+ ULTIMATE_PACKAGE = 'Ultimate'
+
def self.paginated(page: 1)
key = self.cache_key("items:page-#{page}")
@@ -25,14 +29,12 @@ class ReleaseHighlight
file = File.read(file_path)
items = YAML.safe_load(file, permitted_classes: [Date])
- platform = Gitlab.com? ? 'gitlab-com' : 'self-managed'
-
items&.map! do |item|
- next unless item[platform]
+ next unless include_item?(item)
begin
item.tap {|i| i['body'] = Kramdown::Document.new(i['body']).to_html }
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, file_path: file_path)
next
@@ -53,7 +55,8 @@ class ReleaseHighlight
end
def self.cache_key(key)
- ['release_highlight', key, Gitlab.revision].join(':')
+ variant = Gitlab::CurrentSettings.current_application_settings.whats_new_variant
+ ['release_highlight', variant, key, Gitlab.revision].join(':')
end
def self.next_page(current_page: 1)
@@ -88,4 +91,27 @@ class ReleaseHighlight
delegate :each, to: :items
end
+
+ def self.current_package
+ return FREE_PACKAGE unless defined?(License)
+
+ case License.current&.plan&.downcase
+ when License::PREMIUM_PLAN
+ PREMIUM_PACKAGE
+ when License::ULTIMATE_PLAN
+ ULTIMATE_PACKAGE
+ else
+ FREE_PACKAGE
+ end
+ end
+
+ def self.include_item?(item)
+ platform = Gitlab.com? ? 'gitlab-com' : 'self-managed'
+
+ return false unless item[platform]
+
+ return true unless Gitlab::CurrentSettings.current_application_settings.whats_new_variant_current_tier?
+
+ item['packages']&.include?(current_package)
+ end
end
diff --git a/app/models/releases/evidence.rb b/app/models/releases/evidence.rb
index 7c428f5ad03..5fe91b0fef5 100644
--- a/app/models/releases/evidence.rb
+++ b/app/models/releases/evidence.rb
@@ -5,7 +5,7 @@ module Releases
include ShaAttribute
include Presentable
- belongs_to :release, inverse_of: :evidences
+ belongs_to :release, inverse_of: :evidences, touch: true
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
index fc2fa639f56..acc56d3980a 100644
--- a/app/models/releases/link.rb
+++ b/app/models/releases/link.rb
@@ -4,7 +4,7 @@ module Releases
class Link < ApplicationRecord
self.table_name = 'release_links'
- belongs_to :release
+ belongs_to :release, touch: true
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
# Regex modified to prevent catastrophic backtracking
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c7387d2197d..c3ca90ca0ad 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -214,7 +214,7 @@ class RemoteMirror < ApplicationRecord
if super
Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url
end
- rescue
+ rescue StandardError
super
end
@@ -275,7 +275,7 @@ class RemoteMirror < ApplicationRecord
return url unless ssh_key_auth? && password.present?
Gitlab::UrlSanitizer.new(read_attribute(:url), credentials: { user: user }).full_url
- rescue
+ rescue StandardError
super
end
@@ -339,4 +339,4 @@ class RemoteMirror < ApplicationRecord
end
end
-RemoteMirror.prepend_if_ee('EE::RemoteMirror')
+RemoteMirror.prepend_mod_with('RemoteMirror')
diff --git a/app/models/repository.rb b/app/models/repository.rb
index b2efc9b480b..7dca8e52403 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -995,7 +995,13 @@ class Repository
def search_files_by_wildcard_path(path, ref = 'HEAD')
# We need to use RE2 to match Gitaly's regexp engine
- regexp_string = RE2::Regexp.escape(path).gsub('\*', '.*?')
+ regexp_string = RE2::Regexp.escape(path)
+
+ anything = '.*?'
+ anything_but_not_slash = '([^\/])*?'
+ regexp_string.gsub!('\*\*', anything)
+ regexp_string.gsub!('\*', anything_but_not_slash)
+
raw_repository.search_files_by_regexp("^#{regexp_string}$", ref)
end
@@ -1165,17 +1171,13 @@ class Repository
end
def tags_sorted_by_committed_date
- tags.sort_by do |tag|
- # Annotated tags can point to any object (e.g. a blob), but generally
- # tags point to a commit. If we don't have a commit, then just default
- # to putting the tag at the end of the list.
- target = tag.dereferenced_target
+ # Annotated tags can point to any object (e.g. a blob), but generally
+ # tags point to a commit. If we don't have a commit, then just default
+ # to putting the tag at the end of the list.
+ default = Time.current
- if target
- target.committed_date
- else
- Time.current
- end
+ tags.sort_by do |tag|
+ tag.dereferenced_target&.committed_date || default
end
end
@@ -1191,4 +1193,4 @@ class Repository
end
end
-Repository.prepend_if_ee('EE::Repository')
+Repository.prepend_mod_with('Repository')
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 57a3b568c53..68f0ab06bea 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -115,4 +115,4 @@ class ResourceLabelEvent < ResourceEvent
end
end
-ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent')
+ResourceLabelEvent.prepend_mod_with('ResourceLabelEvent')
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 73eb4987143..689a9d8a8ae 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -45,4 +45,4 @@ class ResourceStateEvent < ResourceEvent
end
end
-ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')
+ResourceStateEvent.prepend_mod_with('ResourceStateEvent')
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index 71077758b69..db87ff09159 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -41,4 +41,4 @@ class ResourceTimeboxEvent < ResourceEvent
end
end
-ResourceTimeboxEvent.prepend_if_ee('EE::ResourceTimeboxEvent')
+ResourceTimeboxEvent.prepend_mod_with('ResourceTimeboxEvent')
diff --git a/app/models/serverless/domain_cluster.rb b/app/models/serverless/domain_cluster.rb
index 9f914d5c3f8..0d54a97370e 100644
--- a/app/models/serverless/domain_cluster.rb
+++ b/app/models/serverless/domain_cluster.rb
@@ -12,7 +12,7 @@ module Serverless
attr_encrypted :key,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
validates :pages_domain, :knative, presence: true
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
index 5eca5f2bda1..8a52539d128 100644
--- a/app/models/service_list.rb
+++ b/app/models/service_list.rb
@@ -8,7 +8,7 @@ class ServiceList
end
def to_array
- [Service, columns, values]
+ [Integration, columns, values]
end
private
diff --git a/app/models/sidebars/context.rb b/app/models/sidebars/context.rb
deleted file mode 100644
index d9ac2705aaf..00000000000
--- a/app/models/sidebars/context.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# This class stores all the information needed to display and
-# render the sidebar and menus.
-# It usually stores information regarding the context and calculated
-# values where the logic is in helpers.
-module Sidebars
- class Context
- attr_reader :current_user, :container
-
- def initialize(current_user:, container:, **args)
- @current_user = current_user
- @container = container
-
- args.each do |key, value|
- singleton_class.public_send(:attr_reader, key) # rubocop:disable GitlabSecurity/PublicSend
- instance_variable_set("@#{key}", value)
- end
- end
- end
-end
diff --git a/app/models/sidebars/menu.rb b/app/models/sidebars/menu.rb
deleted file mode 100644
index a5c8be2bb31..00000000000
--- a/app/models/sidebars/menu.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- class Menu
- extend ::Gitlab::Utils::Override
- include ::Gitlab::Routing
- include GitlabRoutingHelper
- include Gitlab::Allowable
- include ::Sidebars::HasPill
- include ::Sidebars::HasIcon
- include ::Sidebars::PositionableList
- include ::Sidebars::Renderable
- include ::Sidebars::ContainerWithHtmlOptions
- include ::Sidebars::HasActiveRoutes
-
- attr_reader :context
- delegate :current_user, :container, to: :@context
-
- def initialize(context)
- @context = context
- @items = []
-
- configure_menu_items
- end
-
- def configure_menu_items
- # No-op
- end
-
- override :render?
- def render?
- @items.empty? || renderable_items.any?
- end
-
- # Menus might have or not a link
- override :link
- def link
- nil
- end
-
- # This method normalizes the information retrieved from the submenus and this menu
- # Value from menus is something like: [{ path: 'foo', path: 'bar', controller: :foo }]
- # This method filters the information and returns: { path: ['foo', 'bar'], controller: :foo }
- def all_active_routes
- @all_active_routes ||= begin
- ([active_routes] + renderable_items.map(&:active_routes)).flatten.each_with_object({}) do |pairs, hash|
- pairs.each do |k, v|
- hash[k] ||= []
- hash[k] += Array(v)
- hash[k].uniq!
- end
-
- hash
- end
- end
- end
-
- def has_items?
- @items.any?
- end
-
- def add_item(item)
- add_element(@items, item)
- end
-
- def insert_item_before(before_item, new_item)
- insert_element_before(@items, before_item, new_item)
- end
-
- def insert_item_after(after_item, new_item)
- insert_element_after(@items, after_item, new_item)
- end
-
- def has_renderable_items?
- renderable_items.any?
- end
-
- def renderable_items
- @renderable_items ||= @items.select(&:render?)
- end
- end
-end
diff --git a/app/models/sidebars/menu_item.rb b/app/models/sidebars/menu_item.rb
deleted file mode 100644
index 7466b31898e..00000000000
--- a/app/models/sidebars/menu_item.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- class MenuItem
- extend ::Gitlab::Utils::Override
- include ::Gitlab::Routing
- include GitlabRoutingHelper
- include Gitlab::Allowable
- include ::Sidebars::HasIcon
- include ::Sidebars::HasHint
- include ::Sidebars::Renderable
- include ::Sidebars::ContainerWithHtmlOptions
- include ::Sidebars::HasActiveRoutes
-
- attr_reader :context
-
- def initialize(context)
- @context = context
- end
- end
-end
diff --git a/app/models/sidebars/panel.rb b/app/models/sidebars/panel.rb
deleted file mode 100644
index 5c8191ebda3..00000000000
--- a/app/models/sidebars/panel.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- class Panel
- extend ::Gitlab::Utils::Override
- include ::Sidebars::PositionableList
-
- attr_reader :context, :scope_menu, :hidden_menu
-
- def initialize(context)
- @context = context
- @scope_menu = nil
- @hidden_menu = nil
- @menus = []
-
- configure_menus
- end
-
- def configure_menus
- # No-op
- end
-
- def add_menu(menu)
- add_element(@menus, menu)
- end
-
- def insert_menu_before(before_menu, new_menu)
- insert_element_before(@menus, before_menu, new_menu)
- end
-
- def insert_menu_after(after_menu, new_menu)
- insert_element_after(@menus, after_menu, new_menu)
- end
-
- def set_scope_menu(scope_menu)
- @scope_menu = scope_menu
- end
-
- def set_hidden_menu(hidden_menu)
- @hidden_menu = hidden_menu
- end
-
- def aria_label
- raise NotImplementedError
- end
-
- def has_renderable_menus?
- renderable_menus.any?
- end
-
- def renderable_menus
- @renderable_menus ||= @menus.select(&:render?)
- end
-
- def container
- context.container
- end
-
- # Auxiliar method that helps with the migration from
- # regular views to the new logic
- def render_raw_scope_menu_partial
- # No-op
- end
-
- # Auxiliar method that helps with the migration from
- # regular views to the new logic.
- #
- # Any menu inside this partial will be added after
- # all the menus added in the `configure_menus`
- # method.
- def render_raw_menus_partial
- # No-op
- end
- end
-end
diff --git a/app/models/sidebars/projects/context.rb b/app/models/sidebars/projects/context.rb
deleted file mode 100644
index 4c82309035d..00000000000
--- a/app/models/sidebars/projects/context.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- class Context < ::Sidebars::Context
- def initialize(current_user:, container:, **args)
- super(current_user: current_user, container: container, project: container, **args)
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/learn_gitlab/menu.rb b/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
deleted file mode 100644
index 4b572846d1a..00000000000
--- a/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module LearnGitlab
- class Menu < ::Sidebars::Menu
- override :link
- def link
- project_learn_gitlab_path(context.project)
- end
-
- override :active_routes
- def active_routes
- { controller: :learn_gitlab }
- end
-
- override :title
- def title
- _('Learn GitLab')
- end
-
- override :extra_container_html_options
- def nav_link_html_options
- { class: 'home' }
- end
-
- override :sprite_icon
- def sprite_icon
- 'home'
- end
-
- override :render?
- def render?
- context.learn_gitlab_experiment_enabled
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu.rb b/app/models/sidebars/projects/menus/project_overview/menu.rb
deleted file mode 100644
index e6aa8ed159f..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- class Menu < ::Sidebars::Menu
- override :configure_menu_items
- def configure_menu_items
- add_item(MenuItems::Details.new(context))
- add_item(MenuItems::Activity.new(context))
- add_item(MenuItems::Releases.new(context))
- end
-
- override :link
- def link
- project_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-project rspec-project-link'
- }
- end
-
- override :extra_container_html_options
- def nav_link_html_options
- { class: 'home' }
- end
-
- override :title
- def title
- _('Project overview')
- end
-
- override :sprite_icon
- def sprite_icon
- 'home'
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
deleted file mode 100644
index 46d0f0bc43b..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- module MenuItems
- class Activity < ::Sidebars::MenuItem
- override :link
- def link
- activity_project_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-project-activity'
- }
- end
-
- override :active_routes
- def active_routes
- { path: 'projects#activity' }
- end
-
- override :title
- def title
- _('Activity')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
deleted file mode 100644
index c40c2ed8fa2..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- module MenuItems
- class Details < ::Sidebars::MenuItem
- override :link
- def link
- project_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- aria: { label: _('Project details') },
- class: 'shortcuts-project'
- }
- end
-
- override :active_routes
- def active_routes
- { path: 'projects#show' }
- end
-
- override :title
- def title
- _('Details')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
deleted file mode 100644
index 5e8348f4398..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- module MenuItems
- class Releases < ::Sidebars::MenuItem
- override :link
- def link
- project_releases_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-project-releases'
- }
- end
-
- override :render?
- def render?
- can?(context.current_user, :read_release, context.project) && !context.project.empty_repo?
- end
-
- override :active_routes
- def active_routes
- { controller: :releases }
- end
-
- override :title
- def title
- _('Releases')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu.rb b/app/models/sidebars/projects/menus/repository/menu.rb
deleted file mode 100644
index f49a0479521..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- class Menu < ::Sidebars::Menu
- override :configure_menu_items
- def configure_menu_items
- add_item(MenuItems::Files.new(context))
- add_item(MenuItems::Commits.new(context))
- add_item(MenuItems::Branches.new(context))
- add_item(MenuItems::Tags.new(context))
- add_item(MenuItems::Contributors.new(context))
- add_item(MenuItems::Graphs.new(context))
- add_item(MenuItems::Compare.new(context))
- end
-
- override :link
- def link
- project_tree_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-tree'
- }
- end
-
- override :title
- def title
- _('Repository')
- end
-
- override :title_html_options
- def title_html_options
- {
- id: 'js-onboarding-repo-link'
- }
- end
-
- override :sprite_icon
- def sprite_icon
- 'doc-text'
- end
-
- override :render?
- def render?
- can?(context.current_user, :download_code, context.project) &&
- !context.project.empty_repo?
- end
- end
- end
- end
- end
-end
-
-Sidebars::Projects::Menus::Repository::Menu.prepend_if_ee('EE::Sidebars::Projects::Menus::Repository::Menu')
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/branches.rb b/app/models/sidebars/projects/menus/repository/menu_items/branches.rb
deleted file mode 100644
index 4a62803dd2b..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/branches.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Branches < ::Sidebars::MenuItem
- override :link
- def link
- project_branches_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- id: 'js-onboarding-branches-link'
- }
- end
-
- override :active_routes
- def active_routes
- { controller: :branches }
- end
-
- override :title
- def title
- _('Branches')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/commits.rb b/app/models/sidebars/projects/menus/repository/menu_items/commits.rb
deleted file mode 100644
index 647cf89133e..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/commits.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Commits < ::Sidebars::MenuItem
- override :link
- def link
- project_commits_path(context.project, context.current_ref)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- id: 'js-onboarding-commits-link'
- }
- end
-
- override :active_routes
- def active_routes
- { controller: %w(commit commits) }
- end
-
- override :title
- def title
- _('Commits')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/compare.rb b/app/models/sidebars/projects/menus/repository/menu_items/compare.rb
deleted file mode 100644
index 4812636b63f..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/compare.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Compare < ::Sidebars::MenuItem
- override :link
- def link
- project_compare_index_path(context.project, from: context.project.repository.root_ref, to: context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { controller: :compare }
- end
-
- override :title
- def title
- _('Compare')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb b/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb
deleted file mode 100644
index d60fd05bb64..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Contributors < ::Sidebars::MenuItem
- override :link
- def link
- project_graph_path(context.project, context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { path: 'graphs#show' }
- end
-
- override :title
- def title
- _('Contributors')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/files.rb b/app/models/sidebars/projects/menus/repository/menu_items/files.rb
deleted file mode 100644
index 4989efe9fa5..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/files.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Files < ::Sidebars::MenuItem
- override :link
- def link
- project_tree_path(context.project, context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { controller: %w[tree blob blame edit_tree new_tree find_file] }
- end
-
- override :title
- def title
- _('Files')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb b/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb
deleted file mode 100644
index a57021be4d0..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Graphs < ::Sidebars::MenuItem
- override :link
- def link
- project_network_path(context.project, context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { controller: :network }
- end
-
- override :title
- def title
- _('Graph')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/tags.rb b/app/models/sidebars/projects/menus/repository/menu_items/tags.rb
deleted file mode 100644
index d84bc89b93c..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/tags.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Tags < ::Sidebars::MenuItem
- override :link
- def link
- project_tags_path(context.project)
- end
-
- override :active_routes
- def active_routes
- { controller: :tags }
- end
-
- override :title
- def title
- _('Tags')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/scope/menu.rb b/app/models/sidebars/projects/menus/scope/menu.rb
deleted file mode 100644
index 3b699083f75..00000000000
--- a/app/models/sidebars/projects/menus/scope/menu.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Scope
- class Menu < ::Sidebars::Menu
- override :link
- def link
- project_path(context.project)
- end
-
- override :title
- def title
- context.project.name
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb
deleted file mode 100644
index ec4fac53a40..00000000000
--- a/app/models/sidebars/projects/panel.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- class Panel < ::Sidebars::Panel
- override :configure_menus
- def configure_menus
- set_scope_menu(Sidebars::Projects::Menus::Scope::Menu.new(context))
-
- add_menu(Sidebars::Projects::Menus::ProjectOverview::Menu.new(context))
- add_menu(Sidebars::Projects::Menus::LearnGitlab::Menu.new(context))
- add_menu(Sidebars::Projects::Menus::Repository::Menu.new(context))
- end
-
- override :render_raw_menus_partial
- def render_raw_menus_partial
- 'layouts/nav/sidebar/project_menus'
- end
-
- override :aria_label
- def aria_label
- _('Project navigation')
- end
- end
- end
-end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5fdd4551982..68957dd6b22 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -20,7 +20,6 @@ class Snippet < ApplicationRecord
extend ::Gitlab::Utils::Override
MAX_FILE_COUNT = 10
- MASTER_BRANCH = 'master'
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -118,7 +117,7 @@ class Snippet < ApplicationRecord
def self.only_include_projects_visible_to(current_user = nil)
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
- joins(:project).where('projects.visibility_level IN (?)', levels)
+ joins(:project).where(projects: { visibility_level: levels })
end
def self.only_include_projects_with_snippets_enabled(include_private: false)
@@ -316,19 +315,19 @@ class Snippet < ApplicationRecord
override :default_branch
def default_branch
- super || MASTER_BRANCH
+ super || Gitlab::DefaultBranch.value(object: project)
end
def repository_storage
snippet_repository&.shard_name || Repository.pick_storage_shard
end
- # Repositories are created by default with the `master` branch.
+ # Repositories are created with a default branch. This branch
+ # can be different from the default branch set in the platform.
# This method changes the `HEAD` file to point to the existing
- # default branch in case it's not master.
+ # default branch in case it's different.
def change_head_to_default_branch
return unless repository.exists?
- return if default_branch == MASTER_BRANCH
# All snippets must have at least 1 file. Therefore, if
# `HEAD` is empty is because it's pointing to the wrong
# default branch
@@ -391,4 +390,4 @@ class Snippet < ApplicationRecord
end
end
-Snippet.prepend_if_ee('EE::Snippet')
+Snippet.prepend_mod_with('Snippet')
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 54dbc579d54..92405a0d943 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -133,4 +133,4 @@ class SnippetRepository < ApplicationRecord
end
end
-SnippetRepository.prepend_if_ee('EE::SnippetRepository')
+SnippetRepository.prepend_mod_with('SnippetRepository')
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index 7e34988c7a0..bb928118edf 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -128,10 +128,10 @@ class SshHostKey
def normalize_url(url)
full_url = ::Addressable::URI.parse(url)
- raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh'
+ raise ArgumentError, "Invalid URL" unless full_url&.scheme == 'ssh'
Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}")
rescue Addressable::URI::InvalidURIError
- raise ArgumentError.new("Invalid URL")
+ raise ArgumentError, "Invalid URL"
end
end
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index f643d52587e..092e5249a3e 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -34,7 +34,7 @@ module Storage
begin
gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
return true
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}")
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 20107147b4f..749b9dce97c 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -41,4 +41,4 @@ class SystemNoteMetadata < ApplicationRecord
end
end
-SystemNoteMetadata.prepend_if_ee('EE::SystemNoteMetadata')
+SystemNoteMetadata.prepend_mod_with('SystemNoteMetadata')
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index eb7d465d585..8aeeae1330c 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -104,3 +104,5 @@ module Terraform
end
end
end
+
+Terraform::State.prepend_mod
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index 432ac5b6422..31ff7e4c27d 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -20,4 +20,4 @@ module Terraform
end
end
-Terraform::StateVersion.prepend_if_ee('EE::Terraform::StateVersion')
+Terraform::StateVersion.prepend_mod_with('Terraform::StateVersion')
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index c1aa84cbbcd..bd543526685 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -3,20 +3,19 @@
class Timelog < ApplicationRecord
include Importable
+ before_save :set_project
+
validates :time_spent, :user, presence: true
validate :issuable_id_is_present, unless: :importing?
belongs_to :issue, touch: true
belongs_to :merge_request, touch: true
+ belongs_to :project
belongs_to :user
belongs_to :note
- scope :for_issues_in_group, -> (group) do
- joins(:issue).where(
- 'EXISTS (?)',
- Project.select(1).where(namespace: group.self_and_descendants)
- .where('issues.project_id = projects.id')
- )
+ scope :in_group, -> (group) do
+ joins(:project).where(projects: { namespace: group.self_and_descendants })
end
scope :between_times, -> (start_time, end_time) do
@@ -37,6 +36,10 @@ class Timelog < ApplicationRecord
end
end
+ def set_project
+ self.project_id = issuable.project_id
+ end
+
# Rails5 defaults to :touch_later, overwrite for normal touch
def belongs_to_touch_method
:touch
diff --git a/app/models/todo.rb b/app/models/todo.rb
index c8138587d83..23685fb68e0 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -149,8 +149,8 @@ class Todo < ApplicationRecord
.order('todos.created_at')
end
- def pluck_user_id
- pluck(:user_id)
+ def distinct_user_ids
+ distinct.pluck(:user_id)
end
# Count todos grouped by user_id and state, using an UNION query
@@ -252,4 +252,4 @@ class Todo < ApplicationRecord
end
end
-Todo.prepend_if_ee('EE::Todo')
+Todo.prepend_mod_with('Todo')
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 46ae924bf8c..0a4acdfc7e3 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -163,4 +163,4 @@ class Upload < ApplicationRecord
end
end
-Upload.prepend_if_ee('EE::Upload')
+Upload.prepend_mod_with('Upload')
diff --git a/app/models/user.rb b/app/models/user.rb
index 507e8cc2cf5..0eb58baae11 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -33,6 +33,8 @@ class User < ApplicationRecord
BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'
+ COUNT_CACHE_VALIDITY_PERIOD = 24.hours
+
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token
@@ -94,6 +96,12 @@ class User < ApplicationRecord
# Virtual attribute for impersonator
attr_accessor :impersonator
+ attr_writer :max_access_for_group
+
+ def max_access_for_group
+ @max_access_for_group ||= {}
+ end
+
#
# Relations
#
@@ -197,6 +205,7 @@ class User < ApplicationRecord
has_one :user_detail
has_one :user_highest_role
has_one :user_canonical_email
+ has_one :credit_card_validation, class_name: '::Users::CreditCardValidation'
has_one :atlassian_identity, class_name: 'Atlassian::Identity'
has_many :reviews, foreign_key: :author_id, inverse_of: :author
@@ -309,6 +318,7 @@ class User < ApplicationRecord
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
+ accepts_nested_attributes_for :credit_card_validation, update_only: true
state_machine :state, initial: :active do
event :block do
@@ -316,6 +326,7 @@ class User < ApplicationRecord
transition deactivated: :blocked
transition ldap_blocked: :blocked
transition blocked_pending_approval: :blocked
+ transition banned: :blocked
end
event :ldap_block do
@@ -328,17 +339,24 @@ class User < ApplicationRecord
transition blocked: :active
transition ldap_blocked: :active
transition blocked_pending_approval: :active
+ transition banned: :active
end
event :block_pending_approval do
transition active: :blocked_pending_approval
end
+ event :ban do
+ transition active: :banned
+ end
+
event :deactivate do
+ # Any additional changes to this event should be also
+ # reflected in app/workers/users/deactivate_dormant_users_worker.rb
transition active: :deactivated
end
- state :blocked, :ldap_blocked, :blocked_pending_approval do
+ state :blocked, :ldap_blocked, :blocked_pending_approval, :banned do
def blocked?
true
end
@@ -365,6 +383,7 @@ class User < ApplicationRecord
scope :instance_access_request_approvers_to_be_notified, -> { admins.active.order_recent_sign_in.limit(INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) }
+ scope :banned, -> { with_states(:banned) }
scope :external, -> { where(external: true) }
scope :non_external, -> { where(external: false) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
@@ -376,7 +395,7 @@ class User < ApplicationRecord
scope :by_name, -> (names) { iwhere(name: Array(names)) }
scope :by_user_email, -> (emails) { iwhere(email: Array(emails)) }
scope :by_emails, -> (emails) { joins(:emails).where(emails: { email: Array(emails).map(&:downcase) }) }
- scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
+ scope :for_todos, -> (todos) { where(id: todos.select(:user_id).distinct) }
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
@@ -416,10 +435,12 @@ class User < ApplicationRecord
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) }
+ scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
+ scope :with_no_activity, -> { active.where(last_activity_on: nil) }
def preferred_language
read_attribute('preferred_language') ||
- I18n.default_locale.to_s.presence_in(Gitlab::I18n::AVAILABLE_LANGUAGES.keys) ||
+ I18n.default_locale.to_s.presence_in(Gitlab::I18n.available_locales) ||
'en'
end
@@ -584,6 +605,8 @@ class User < ApplicationRecord
blocked
when 'blocked_pending_approval'
blocked_pending_approval
+ when 'banned'
+ banned
when 'two_factor_disabled'
without_two_factor
when 'two_factor_enabled'
@@ -1098,6 +1121,11 @@ class User < ApplicationRecord
Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !password_based_omniauth_user?
end
+ # method overriden in EE
+ def password_based_login_forbidden?
+ false
+ end
+
def can_change_username?
gitlab_config.username_changing_enabled
end
@@ -1211,6 +1239,10 @@ class User < ApplicationRecord
user_highest_role&.highest_access_level || Gitlab::Access::NO_ACCESS
end
+ def credit_card_validated_at
+ credit_card_validation&.credit_card_validated_at
+ end
+
def accessible_deploy_keys
DeployKey.from_union([
DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
@@ -1414,7 +1446,9 @@ class User < ApplicationRecord
if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username)
self.errors.add(:base, :username_exists_as_a_different_namespace)
else
- self.errors[:username].concat(namespace_path_errors)
+ namespace_path_errors.each do |msg|
+ self.errors.add(:username, msg)
+ end
end
end
@@ -1619,40 +1653,32 @@ class User < ApplicationRecord
@global_notification_setting
end
- def count_cache_validity_period
- if Feature.enabled?(:longer_count_cache_validity, self, default_enabled: :yaml)
- 24.hours
- else
- 20.minutes
- end
- end
-
def assigned_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def review_requested_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count
end
end
def assigned_open_issues_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def todos_done_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
TodosFinder.new(self, state: :done).execute.count
end
end
def todos_pending_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
TodosFinder.new(self, state: :pending).execute.count
end
end
@@ -1677,6 +1703,12 @@ class User < ApplicationRecord
def invalidate_issue_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
+
+ if Feature.enabled?(:assigned_open_issues_cache, default_enabled: :yaml)
+ run_after_commit do
+ Users::UpdateOpenIssueCountWorker.perform_async(self.id)
+ end
+ end
end
def invalidate_merge_request_cache_counts
@@ -2061,4 +2093,4 @@ class User < ApplicationRecord
end
end
-User.prepend_if_ee('EE::User')
+User.prepend_mod_with('User')
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 0a4db707be6..8fc9efddac9 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -30,7 +30,9 @@ class UserCallout < ApplicationRecord
new_user_signups_cap_reached: 26, # EE-only
unfinished_tag_cleanup_callout: 27,
eoa_bronze_plan_banner: 28, # EE-only
- pipeline_needs_banner: 29
+ pipeline_needs_banner: 29,
+ pipeline_needs_hover_tip: 30,
+ web_ide_ci_environments_guidance: 31
}
validates :user, presence: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 6b64f583927..458764632ed 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -32,4 +32,4 @@ class UserDetail < ApplicationRecord
end
end
-UserDetail.prepend_if_ee('EE::UserDetail')
+UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 0bf8c8f901d..2735e169b5f 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -71,4 +71,4 @@ class UserPreference < ApplicationRecord
end
end
-UserPreference.prepend_if_ee('EE::UserPreference')
+UserPreference.prepend_mod_with('UserPreference')
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
new file mode 100644
index 00000000000..5e255acd882
--- /dev/null
+++ b/app/models/users/credit_card_validation.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class CreditCardValidation < ApplicationRecord
+ RELEASE_DAY = Date.new(2021, 5, 17)
+
+ self.table_name = 'user_credit_card_validations'
+
+ belongs_to :user
+ end
+end
diff --git a/app/models/users/merge_request_interaction.rb b/app/models/users/merge_request_interaction.rb
index 35d1d3206b5..4af9361fbf6 100644
--- a/app/models/users/merge_request_interaction.rb
+++ b/app/models/users/merge_request_interaction.rb
@@ -41,4 +41,4 @@ module Users
end
end
-::Users::MergeRequestInteraction.prepend_if_ee('EE::Users::MergeRequestInteraction')
+::Users::MergeRequestInteraction.prepend_mod_with('Users::MergeRequestInteraction')
diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb
index d724b06a996..a903541f69a 100644
--- a/app/models/users_statistics.rb
+++ b/app/models/users_statistics.rb
@@ -71,4 +71,4 @@ class UsersStatistics < ApplicationRecord
end
end
-UsersStatistics.prepend_if_ee('EE::UsersStatistics')
+UsersStatistics.prepend_mod_with('UsersStatistics')
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index 7728c9c174e..4e1f48227d9 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -17,4 +17,4 @@ class Vulnerability < ApplicationRecord
end
end
-Vulnerability.prepend_ee_mod
+Vulnerability.prepend_mod
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 47fe40b0e57..7fc01f373c8 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -88,7 +88,7 @@ class Wiki
repository.create_if_not_exists
raise CouldNotCreateWikiError unless repository_exists?
- rescue => err
+ rescue StandardError => err
Gitlab::ErrorTracking.track_exception(err, wiki: {
container_type: container.class.name,
container_id: container.id,
@@ -192,16 +192,9 @@ class Wiki
def delete_page(page, message = nil)
return unless page
- if Feature.enabled?(:gitaly_replace_wiki_delete_page, user, default_enabled: :yaml)
- capture_git_error(:deleted) do
- repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
+ capture_git_error(:deleted) do
+ repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
- after_wiki_activity
-
- true
- end
- else
- wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
after_wiki_activity
true
@@ -327,4 +320,4 @@ class Wiki
end
end
-Wiki.prepend_if_ee('EE::Wiki')
+Wiki.prepend_mod_with('Wiki')
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 3b9a7ded83e..9ae5a870323 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -127,10 +127,21 @@ class WikiPage
@path ||= @page.path
end
+ # Returns a CommitCollection
+ #
+ # Queries the commits for current page's path, equivalent to
+ # `git log path/to/page`. Filters and options supported:
+ # https://gitlab.com/gitlab-org/gitaly/-/blob/master/proto/commit.proto#L322-344
def versions(options = {})
return [] unless persisted?
- wiki.wiki.page_versions(page.path, options)
+ default_per_page = Kaminari.config.default_per_page
+ offset = [options[:page].to_i - 1, 0].max * options.fetch(:per_page, default_per_page)
+
+ wiki.repository.commits('HEAD',
+ path: page.path,
+ limit: options.fetch(:limit, default_per_page),
+ offset: offset)
end
def count_versions