Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/analytics/cycle_analytics/project_level.rb49
-rw-r--r--app/models/analytics/usage_trends/measurement.rb2
-rw-r--r--app/models/application_record.rb10
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/models/bulk_imports/export.rb10
-rw-r--r--app/models/bulk_imports/export_status.rb47
-rw-r--r--app/models/bulk_imports/file_transfer/base_config.rb12
-rw-r--r--app/models/chat_team.rb4
-rw-r--r--app/models/ci/bridge.rb2
-rw-r--r--app/models/ci/build.rb132
-rw-r--r--app/models/ci/build_dependencies.rb2
-rw-r--r--app/models/ci/build_metadata.rb7
-rw-r--r--app/models/ci/build_trace_chunk.rb33
-rw-r--r--app/models/ci/build_trace_chunks/database.rb4
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb12
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb87
-rw-r--r--app/models/ci/build_trace_chunks/redis_base.rb90
-rw-r--r--app/models/ci/build_trace_chunks/redis_trace_chunks.rb13
-rw-r--r--app/models/ci/build_trace_section.rb3
-rw-r--r--app/models/ci/job_artifact.rb18
-rw-r--r--app/models/ci/job_token/project_scope_link.rb33
-rw-r--r--app/models/ci/job_token/scope.rb43
-rw-r--r--app/models/ci/pending_build.rb18
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/ci/pipeline_schedule.rb12
-rw-r--r--app/models/ci/processable.rb2
-rw-r--r--app/models/ci/runner.rb98
-rw-r--r--app/models/ci/runner_namespace.rb5
-rw-r--r--app/models/ci/runner_project.rb5
-rw-r--r--app/models/ci/running_build.rb28
-rw-r--r--app/models/ci/stage.rb3
-rw-r--r--app/models/clusters/applications/fluentd.rb121
-rw-r--r--app/models/clusters/applications/ingress.rb101
-rw-r--r--app/models/clusters/applications/knative.rb4
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb33
-rw-r--r--app/models/clusters/clusters_hierarchy.rb9
-rw-r--r--app/models/commit.rb34
-rw-r--r--app/models/commit_status.rb8
-rw-r--r--app/models/concerns/bulk_insert_safe.rb6
-rw-r--r--app/models/concerns/cache_markdown_field.rb9
-rw-r--r--app/models/concerns/cron_schedulable.rb21
-rw-r--r--app/models/concerns/deployment_platform.rb8
-rw-r--r--app/models/concerns/enum_with_nil.rb8
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb1
-rw-r--r--app/models/concerns/enums/vulnerability.rb12
-rw-r--r--app/models/concerns/has_timelogs_report.rb20
-rw-r--r--app/models/concerns/has_user_type.rb5
-rw-r--r--app/models/concerns/integrations/base_data_fields.rb (renamed from app/models/concerns/services/data_fields.rb)8
-rw-r--r--app/models/concerns/integrations/has_data_fields.rb61
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb (renamed from app/models/project_services/slack_mattermost/notifier.rb)6
-rw-r--r--app/models/concerns/issuable.rb23
-rw-r--r--app/models/concerns/issue_available_features.rb3
-rw-r--r--app/models/concerns/limitable.rb3
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/concerns/notification_branch_selection.rb2
-rw-r--r--app/models/concerns/packages/debian/component_file.rb4
-rw-r--r--app/models/concerns/packages/debian/distribution.rb12
-rw-r--r--app/models/concerns/packages/debian/distribution_key.rb45
-rw-r--r--app/models/concerns/prometheus_adapter.rb2
-rw-r--r--app/models/concerns/service_push_data_validations.rb4
-rw-r--r--app/models/concerns/taggable_queries.rb16
-rw-r--r--app/models/concerns/time_trackable.rb18
-rw-r--r--app/models/concerns/timebox.rb13
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encryption_helper.rb12
-rw-r--r--app/models/container_repository.rb23
-rw-r--r--app/models/cycle_analytics/project_level.rb48
-rw-r--r--app/models/deployment.rb3
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/environment_status.rb2
-rw-r--r--app/models/experiment.rb13
-rw-r--r--app/models/experiment_subject.rb10
-rw-r--r--app/models/group.rb56
-rw-r--r--app/models/group_deploy_token.rb2
-rw-r--r--app/models/hooks/project_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb16
-rw-r--r--app/models/hooks/web_hook_log_archived.rb12
-rw-r--r--app/models/import_export_upload.rb35
-rw-r--r--app/models/integration.rb54
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb255
-rw-r--r--app/models/integrations/base_ci.rb44
-rw-r--r--app/models/integrations/base_issue_tracker.rb156
-rw-r--r--app/models/integrations/base_slash_commands.rb67
-rw-r--r--app/models/integrations/bugzilla.rb26
-rw-r--r--app/models/integrations/buildkite.rb145
-rw-r--r--app/models/integrations/builds_email.rb16
-rw-r--r--app/models/integrations/chat_message/base_message.rb2
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb8
-rw-r--r--app/models/integrations/chat_message/push_message.rb2
-rw-r--r--app/models/integrations/chat_message/wiki_page_message.rb12
-rw-r--r--app/models/integrations/custom_issue_tracker.rb25
-rw-r--r--app/models/integrations/discord.rb68
-rw-r--r--app/models/integrations/drone_ci.rb106
-rw-r--r--app/models/integrations/ewm.rb38
-rw-r--r--app/models/integrations/external_wiki.rb52
-rw-r--r--app/models/integrations/flowdock.rb52
-rw-r--r--app/models/integrations/hangouts_chat.rb71
-rw-r--r--app/models/integrations/irker.rb123
-rw-r--r--app/models/integrations/issue_tracker_data.rb11
-rw-r--r--app/models/integrations/jenkins.rb113
-rw-r--r--app/models/integrations/jira.rb588
-rw-r--r--app/models/integrations/jira_tracker_data.rb14
-rw-r--r--app/models/integrations/mattermost.rb33
-rw-r--r--app/models/integrations/mattermost_slash_commands.rb59
-rw-r--r--app/models/integrations/microsoft_teams.rb59
-rw-r--r--app/models/integrations/mock_ci.rb90
-rw-r--r--app/models/integrations/open_project.rb20
-rw-r--r--app/models/integrations/open_project_tracker_data.rb18
-rw-r--r--app/models/integrations/packagist.rb67
-rw-r--r--app/models/integrations/pipelines_email.rb105
-rw-r--r--app/models/integrations/pivotaltracker.rb78
-rw-r--r--app/models/integrations/pushover.rb107
-rw-r--r--app/models/integrations/redmine.rb25
-rw-r--r--app/models/integrations/slack.rb59
-rw-r--r--app/models/integrations/slack_slash_commands.rb36
-rw-r--r--app/models/integrations/teamcity.rb191
-rw-r--r--app/models/integrations/unify_circuit.rb62
-rw-r--r--app/models/integrations/webex_teams.rb56
-rw-r--r--app/models/integrations/youtrack.rb42
-rw-r--r--app/models/issue.rb6
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/label_link.rb1
-rw-r--r--app/models/lfs_object.rb17
-rw-r--r--app/models/member.rb23
-rw-r--r--app/models/members/group_member.rb6
-rw-r--r--app/models/members/last_group_owner_assigner.rb62
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_diff.rb18
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/milestone.rb13
-rw-r--r--app/models/namespace.rb15
-rw-r--r--app/models/namespace_setting.rb3
-rw-r--r--app/models/namespaces/traversal/linear.rb27
-rw-r--r--app/models/namespaces/traversal/recursive.rb6
-rw-r--r--app/models/note.rb10
-rw-r--r--app/models/onboarding_progress.rb4
-rw-r--r--app/models/operations/feature_flag.rb2
-rw-r--r--app/models/packages/debian/group_distribution_key.rb9
-rw-r--r--app/models/packages/debian/project_distribution_key.rb9
-rw-r--r--app/models/packages/package.rb71
-rw-r--r--app/models/packages/package_file.rb7
-rw-r--r--app/models/pages/lookup_path.rb40
-rw-r--r--app/models/pages_domain.rb12
-rw-r--r--app/models/postgresql/replication_slot.rb4
-rw-r--r--app/models/preloaders/user_max_access_level_in_projects_preloader.rb2
-rw-r--r--app/models/project.rb220
-rw-r--r--app/models/project_authorization.rb9
-rw-r--r--app/models/project_ci_cd_setting.rb1
-rw-r--r--app/models/project_feature.rb8
-rw-r--r--app/models/project_feature_usage.rb14
-rw-r--r--app/models/project_repository_storage_move.rb13
-rw-r--r--app/models/project_services/bugzilla_service.rb24
-rw-r--r--app/models/project_services/buildkite_service.rb143
-rw-r--r--app/models/project_services/chat_notification_service.rb252
-rw-r--r--app/models/project_services/ci_service.rb42
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb23
-rw-r--r--app/models/project_services/data_fields.rb59
-rw-r--r--app/models/project_services/discord_service.rb66
-rw-r--r--app/models/project_services/drone_ci_service.rb104
-rw-r--r--app/models/project_services/ewm_service.rb36
-rw-r--r--app/models/project_services/external_wiki_service.rb50
-rw-r--r--app/models/project_services/flowdock_service.rb50
-rw-r--r--app/models/project_services/hangouts_chat_service.rb71
-rw-r--r--app/models/project_services/hipchat_service.rb32
-rw-r--r--app/models/project_services/irker_service.rb121
-rw-r--r--app/models/project_services/issue_tracker_data.rb9
-rw-r--r--app/models/project_services/issue_tracker_service.rb152
-rw-r--r--app/models/project_services/jenkins_service.rb111
-rw-r--r--app/models/project_services/jira_service.rb541
-rw-r--r--app/models/project_services/jira_tracker_data.rb24
-rw-r--r--app/models/project_services/mattermost_service.rb31
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb57
-rw-r--r--app/models/project_services/microsoft_teams_service.rb57
-rw-r--r--app/models/project_services/mock_ci_service.rb90
-rw-r--r--app/models/project_services/open_project_service.rb18
-rw-r--r--app/models/project_services/open_project_tracker_data.rb16
-rw-r--r--app/models/project_services/packagist_service.rb65
-rw-r--r--app/models/project_services/pipelines_email_service.rb103
-rw-r--r--app/models/project_services/pivotaltracker_service.rb76
-rw-r--r--app/models/project_services/prometheus_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb105
-rw-r--r--app/models/project_services/redmine_service.rb23
-rw-r--r--app/models/project_services/slack_service.rb57
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb34
-rw-r--r--app/models/project_services/slash_commands_service.rb65
-rw-r--r--app/models/project_services/teamcity_service.rb189
-rw-r--r--app/models/project_services/unify_circuit_service.rb60
-rw-r--r--app/models/project_services/webex_teams_service.rb54
-rw-r--r--app/models/project_services/youtrack_service.rb40
-rw-r--r--app/models/project_statistics.rb20
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/release.rb8
-rw-r--r--app/models/release_highlight.rb2
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/service_desk_setting.rb22
-rw-r--r--app/models/snippet_repository_storage_move.rb13
-rw-r--r--app/models/timelog.rb8
-rw-r--r--app/models/todo.rb1
-rw-r--r--app/models/user.rb57
-rw-r--r--app/models/user_callout.rb5
-rw-r--r--app/models/user_detail.rb1
-rw-r--r--app/models/users/in_product_marketing_email.rb3
206 files changed, 4633 insertions, 4060 deletions
diff --git a/app/models/analytics/cycle_analytics/project_level.rb b/app/models/analytics/cycle_analytics/project_level.rb
new file mode 100644
index 00000000000..7a73bc75ed6
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/project_level.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ProjectLevel
+ attr_reader :project, :options
+
+ def initialize(project:, options:)
+ @project = project
+ @options = options.merge(project: project)
+ end
+
+ def summary
+ @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(project,
+ options: options,
+ current_user: options[:current_user]).data
+ end
+
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
+ end
+
+ def stats
+ @stats ||= default_stage_names.map do |stage_name|
+ self[stage_name].as_json
+ end
+ end
+
+ def [](stage_name)
+ ::CycleAnalytics::ProjectLevelStageAdapter.new(build_stage(stage_name), options)
+ end
+
+ private
+
+ def build_stage(stage_name)
+ stage_params = stage_params_by_name(stage_name).merge(project: project)
+ Analytics::CycleAnalytics::ProjectStage.new(stage_params)
+ end
+
+ def stage_params_by_name(name)
+ Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name)
+ end
+
+ def default_stage_names
+ Gitlab::Analytics::CycleAnalytics::DefaultStages.symbolized_stage_names
+ end
+ end
+ end
+end
diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb
index 46c5d56d210..02e239ca0ef 100644
--- a/app/models/analytics/usage_trends/measurement.rb
+++ b/app/models/analytics/usage_trends/measurement.rb
@@ -3,7 +3,7 @@
module Analytics
module UsageTrends
class Measurement < ApplicationRecord
- self.table_name = 'analytics_instance_statistics_measurements'
+ self.table_name = 'analytics_usage_trends_measurements'
enum identifier: {
projects: 1,
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 5e5bc00458e..a93348a3b27 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -53,10 +53,12 @@ class ApplicationRecord < ActiveRecord::Base
# Start a new transaction with a shorter-than-usual statement timeout. This is
# currently one third of the default 15-second timeout
def self.with_fast_read_statement_timeout(timeout_ms = 5000)
- transaction(requires_new: true) do
- connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ transaction(requires_new: true) do
+ connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
- yield
+ yield
+ end
end
end
@@ -85,5 +87,3 @@ class ApplicationRecord < ActiveRecord::Base
enum(enum_mod.key => values)
end
end
-
-ApplicationRecord.prepend_mod_with('ApplicationRecordHelpers')
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 65800e40d6c..f8047ed9b78 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -273,6 +273,18 @@ class ApplicationSetting < ApplicationRecord
greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND }
+ validates :diff_max_files,
+ presence: true,
+ numericality: { only_integer: true,
+ greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_FILES_SETTING,
+ less_than_or_equal_to: Commit::MAX_DIFF_FILES_SETTING_UPPER_BOUND }
+
+ validates :diff_max_lines,
+ presence: true,
+ numericality: { only_integer: true,
+ greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_LINES_SETTING,
+ less_than_or_equal_to: Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND }
+
validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :personal_access_token_prefix,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index bf9df3b9efc..b613e698471 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -60,6 +60,8 @@ module ApplicationSettingImplementation
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
+ diff_max_files: Commit::DEFAULT_MAX_DIFF_FILES_SETTING,
+ diff_max_lines: Commit::DEFAULT_MAX_DIFF_LINES_SETTING,
disable_feed_token: false,
disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb
index 59ca4dbfec6..371b58dea03 100644
--- a/app/models/bulk_imports/export.rb
+++ b/app/models/bulk_imports/export.rb
@@ -4,6 +4,10 @@ module BulkImports
class Export < ApplicationRecord
include Gitlab::Utils::StrongMemoize
+ STARTED = 0
+ FINISHED = 1
+ FAILED = -1
+
self.table_name = 'bulk_import_exports'
belongs_to :project, optional: true
@@ -18,9 +22,9 @@ module BulkImports
validate :portable_relation?
state_machine :status, initial: :started do
- state :started, value: 0
- state :finished, value: 1
- state :failed, value: -1
+ state :started, value: STARTED
+ state :finished, value: FINISHED
+ state :failed, value: FAILED
event :start do
transition any => :started
diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb
new file mode 100644
index 00000000000..98804d18f27
--- /dev/null
+++ b/app/models/bulk_imports/export_status.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportStatus
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline_tracker, relation)
+ @pipeline_tracker = pipeline_tracker
+ @relation = relation
+ @entity = @pipeline_tracker.entity
+ @configuration = @entity.bulk_import.configuration
+ @client = Clients::HTTP.new(uri: @configuration.url, token: @configuration.access_token)
+ end
+
+ def started?
+ export_status['status'] == Export::STARTED
+ end
+
+ def failed?
+ export_status['status'] == Export::FAILED
+ end
+
+ def error
+ export_status['error']
+ end
+
+ private
+
+ attr_reader :client, :entity, :relation
+
+ def export_status
+ strong_memoize(:export_status) do
+ fetch_export_status.find { |item| item['relation'] == relation }
+ end
+ rescue StandardError => e
+ { 'status' => Export::FAILED, 'error' => e.message }
+ end
+
+ def fetch_export_status
+ client.get(status_endpoint).parsed_response
+ end
+
+ def status_endpoint
+ "/groups/#{entity.encoded_source_full_path}/export_relations/status"
+ 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
index bb04e84ad72..7396f9d3655 100644
--- a/app/models/bulk_imports/file_transfer/base_config.rb
+++ b/app/models/bulk_imports/file_transfer/base_config.rb
@@ -13,6 +13,14 @@ module BulkImports
attributes_finder.find_root(portable_class_sym)
end
+ def top_relation_tree(relation)
+ portable_relations_tree[relation.to_s]
+ end
+
+ def relation_excluded_keys(relation)
+ attributes_finder.find_excluded_keys(relation)
+ end
+
def export_path
strong_memoize(:export_path) do
relative_path = File.join(base_export_path, SecureRandom.hex)
@@ -47,6 +55,10 @@ module BulkImports
@portable_class_sym ||= portable_class.to_s.demodulize.underscore.to_sym
end
+ def portable_relations_tree
+ @portable_relations_tree ||= attributes_finder.find_relations_tree(portable_class_sym).deep_stringify_keys
+ end
+
def import_export_yaml
raise NotImplementedError
end
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
index 6e39d7e2204..ee786ae6cb7 100644
--- a/app/models/chat_team.rb
+++ b/app/models/chat_team.rb
@@ -7,8 +7,8 @@ class ChatTeam < ApplicationRecord
belongs_to :namespace
def remove_mattermost_team(current_user)
- Mattermost::Team.new(current_user).destroy(team_id: team_id)
- rescue Mattermost::ClientError => e
+ ::Mattermost::Team.new(current_user).destroy(team_id: team_id)
+ rescue ::Mattermost::ClientError => e
# Either the group is not found, or the user doesn't have the proper
# access on the mattermost instance. In the first case, we're done either way
# in the latter case, we can't recover by retrying, so we just log what happened
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 352229c64da..577bca282ef 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -163,7 +163,7 @@ module Ci
def expanded_environment_name
end
- def instantized_environment
+ def persisted_environment
end
def execute_hooks
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 46fc87a6ea8..fdfffd9b0cd 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,6 +11,7 @@ module Ci
include Importable
include Ci::HasRef
include IgnorableColumns
+ include TaggableQueries
BuildArchivedError = Class.new(StandardError)
@@ -37,6 +38,8 @@ module Ci
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
+ has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id
+ has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
@@ -88,16 +91,6 @@ 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
@@ -212,6 +205,8 @@ module Ci
end
scope :with_coverage, -> { where.not(coverage: nil) }
+ scope :without_coverage, -> { where(coverage: nil) }
+ scope :with_coverage_regex, -> { where.not(coverage_regex: nil) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
@@ -222,6 +217,8 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
+ after_save :stick_build_if_status_changed
+
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
@@ -304,12 +301,35 @@ module Ci
end
end
- after_transition any => [:pending] do |build|
+ # rubocop:disable CodeReuse/ServiceClass
+ after_transition any => [:pending] do |build, transition|
+ Ci::UpdateBuildQueueService.new.push(build, transition)
+
build.run_after_commit do
BuildQueueWorker.perform_async(id)
end
end
+ after_transition pending: any do |build, transition|
+ Ci::UpdateBuildQueueService.new.pop(build, transition)
+ end
+
+ after_transition any => [:running] do |build, transition|
+ Ci::UpdateBuildQueueService.new.track(build, transition)
+ end
+
+ after_transition running: any do |build, transition|
+ Ci::UpdateBuildQueueService.new.untrack(build, transition)
+
+ Ci::BuildRunnerSession.where(build: build).delete_all
+ end
+
+ # rubocop:enable CodeReuse/ServiceClass
+ #
+ after_transition pending: :running do |build|
+ build.ensure_metadata.update_timeout_state
+ end
+
after_transition pending: :running do |build|
build.deployment&.run
@@ -362,14 +382,6 @@ module Ci
end
end
- after_transition pending: :running do |build|
- build.ensure_metadata.update_timeout_state
- end
-
- after_transition running: any do |build|
- Ci::BuildRunnerSession.where(build: build).delete_all
- end
-
after_transition any => [:skipped, :canceled] do |build, transition|
if transition.to_name == :skipped
build.deployment&.skip
@@ -379,6 +391,33 @@ module Ci
end
end
+ def self.build_matchers(project)
+ unique_params = [
+ :protected,
+ Arel.sql("(#{arel_tag_names_array.to_sql})")
+ ]
+
+ group(*unique_params).pluck('array_agg(id)', *unique_params).map do |values|
+ Gitlab::Ci::Matching::BuildMatcher.new({
+ build_ids: values[0],
+ protected: values[1],
+ tag_list: values[2],
+ project: project
+ })
+ end
+ end
+
+ def build_matcher
+ strong_memoize(:build_matcher) do
+ Gitlab::Ci::Matching::BuildMatcher.new({
+ protected: protected?,
+ tag_list: tag_list,
+ build_ids: [id],
+ project: project
+ })
+ end
+ end
+
def auto_retry_allowed?
auto_retry.allowed?
end
@@ -442,7 +481,13 @@ module Ci
end
def retryable?
- !archived? && (success? || failed? || canceled?)
+ if Feature.enabled?(:prevent_retry_of_retried_jobs, project, default_enabled: :yaml)
+ return false if retried? || archived?
+
+ success? || failed? || canceled?
+ else
+ !archived? && (success? || failed? || canceled?)
+ end
end
def retries_count
@@ -560,6 +605,8 @@ module Ci
variables.concat(persisted_environment.predefined_variables)
+ variables.append(key: 'CI_ENVIRONMENT_ACTION', value: environment_action)
+
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
@@ -716,22 +763,14 @@ module Ci
end
def any_runners_online?
- if Feature.enabled?(:runners_cached_states, project, default_enabled: :yaml)
- cache_for_online_runners do
- project.any_online_runners? { |runner| runner.match_build_if_online?(self) }
- end
- else
- project.any_active_runners? { |runner| runner.match_build_if_online?(self) }
+ cache_for_online_runners do
+ project.any_online_runners? { |runner| runner.match_build_if_online?(self) }
end
end
def any_runners_available?
- if Feature.enabled?(:runners_cached_states, project, default_enabled: :yaml)
- cache_for_available_runners do
- project.active_runners.exists?
- end
- else
- project.any_active_runners?
+ cache_for_available_runners do
+ project.active_runners.exists?
end
end
@@ -1039,6 +1078,28 @@ module Ci
options.dig(:allow_failure_criteria, :exit_codes).present?
end
+ def create_queuing_entry!
+ ::Ci::PendingBuild.upsert_from_build!(self)
+ end
+
+ ##
+ # We can have only one queuing entry or running build tracking entry,
+ # because there is a unique index on `build_id` in each table, but we need
+ # a relation to remove these entries more efficiently in a single statement
+ # without actually loading data.
+ #
+ def all_queuing_entries
+ ::Ci::PendingBuild.where(build_id: self.id)
+ end
+
+ def all_runtime_metadata
+ ::Ci::RunningBuild.where(build_id: self.id)
+ end
+
+ def shared_runner_build?
+ runner&.instance_type?
+ end
+
protected
def run_status_commit_hooks!
@@ -1049,6 +1110,13 @@ module Ci
private
+ def stick_build_if_status_changed
+ return unless saved_change_to_status?
+ return unless running?
+
+ ::Gitlab::Database::LoadBalancing::Sticking.stick(:build, id)
+ end
+
def status_commit_hooks
@status_commit_hooks ||= []
end
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index 716d919487d..d39e0411a79 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -143,8 +143,6 @@ module Ci
def specified_cross_pipeline_dependencies
strong_memoize(:specified_cross_pipeline_dependencies) do
- next [] unless Feature.enabled?(:ci_cross_pipeline_artifacts_download, processable.project, default_enabled: true)
-
specified_cross_dependencies.select { |dep| dep[:pipeline] && dep[:artifacts] }
end
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 4094bdb26dc..bb2dac5cd43 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -10,6 +10,7 @@ module Ci
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
+ include IgnorableColumns
self.table_name = 'ci_builds_metadata'
@@ -21,8 +22,8 @@ module Ci
validates :build, presence: true
validates :secrets, json_schema: { filename: 'build_metadata_secrets' }
- serialize :config_options, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
- serialize :config_variables, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :config_options, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :config_variables, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout
@@ -37,6 +38,8 @@ module Ci
job_timeout_source: 4
}
+ ignore_column :build_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
+
def update_timeout_state
timeout = timeout_with_highest_precedence
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 719511bbb8a..25f4a06088d 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -14,7 +14,13 @@ module Ci
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
- default_value_for :data_store, :redis
+ default_value_for :data_store do
+ if Feature.enabled?(:dedicated_redis_trace_chunks, type: :ops)
+ :redis_trace_chunks
+ else
+ :redis
+ end
+ end
after_create { metrics.increment_trace_operation(operation: :chunked) }
@@ -25,22 +31,22 @@ module Ci
FailedToPersistDataError = Class.new(StandardError)
- # Note: The ordering of this hash is related to the precedence of persist store.
- # The bottom item takes the highest precedence, and the top item takes the lowest precedence.
DATA_STORES = {
redis: 1,
database: 2,
- fog: 3
+ fog: 3,
+ redis_trace_chunks: 4
}.freeze
STORE_TYPES = DATA_STORES.keys.to_h do |store|
- [store, "Ci::BuildTraceChunks::#{store.capitalize}".constantize]
+ [store, "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize]
end.freeze
+ LIVE_STORES = %i[redis redis_trace_chunks].freeze
enum data_store: DATA_STORES
- scope :live, -> { redis }
- scope :persisted, -> { not_redis.order(:chunk_index) }
+ scope :live, -> { where(data_store: LIVE_STORES) }
+ scope :persisted, -> { where.not(data_store: LIVE_STORES).order(:chunk_index) }
class << self
def all_stores
@@ -48,8 +54,7 @@ module Ci
end
def persistable_store
- # get first available store from the back of the list
- all_stores.reverse.find { |store| get_store_class(store).available? }
+ STORE_TYPES[:fog].available? ? :fog : :database
end
def get_store_class(store)
@@ -85,16 +90,10 @@ module Ci
# change the behavior in CE.
#
def with_read_consistency(build, &block)
- return yield unless consistent_reads_enabled?(build)
-
::Gitlab::Database::Consistency
.with_read_consistency(&block)
end
- def consistent_reads_enabled?(build)
- Feature.enabled?(:gitlab_ci_trace_read_consistency, build.project, type: :development, default_enabled: true)
- end
-
##
# Sometimes we do not want to read raw data. This method makes it easier
# to find attributes that are just metadata excluding raw data.
@@ -201,7 +200,7 @@ module Ci
end
def flushed?
- !redis?
+ !live?
end
def migrated?
@@ -209,7 +208,7 @@ module Ci
end
def live?
- redis?
+ LIVE_STORES.include?(data_store.to_sym)
end
def <=>(other)
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
index 7448afba4c2..895028778a9 100644
--- a/app/models/ci/build_trace_chunks/database.rb
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -3,10 +3,6 @@
module Ci
module BuildTraceChunks
class Database
- def available?
- true
- end
-
def keys(relation)
[]
end
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
index cbf0c0a1696..fab85fae33d 100644
--- a/app/models/ci/build_trace_chunks/fog.rb
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -3,10 +3,18 @@
module Ci
module BuildTraceChunks
class Fog
- def available?
+ def self.available?
object_store.enabled
end
+ def self.object_store
+ Gitlab.config.artifacts.object_store
+ end
+
+ def available?
+ self.class.available?
+ end
+
def data(model)
files.get(key(model))&.body
rescue Excon::Error::NotFound
@@ -85,7 +93,7 @@ module Ci
end
def object_store
- Gitlab.config.artifacts.object_store
+ self.class.object_store
end
def object_store_raw_config
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
index 003ec107895..46f275636e1 100644
--- a/app/models/ci/build_trace_chunks/redis.rb
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -2,92 +2,11 @@
module Ci
module BuildTraceChunks
- class Redis
- CHUNK_REDIS_TTL = 1.week
- LUA_APPEND_CHUNK = <<~EOS
- local key, new_data, offset = KEYS[1], ARGV[1], ARGV[2]
- local length = new_data:len()
- local expire = #{CHUNK_REDIS_TTL.seconds}
- local current_size = redis.call("strlen", key)
- offset = tonumber(offset)
-
- if offset == 0 then
- -- overwrite everything
- redis.call("set", key, new_data, "ex", expire)
- return redis.call("strlen", key)
- elseif offset > current_size then
- -- offset range violation
- return -1
- elseif offset + length >= current_size then
- -- efficiently append or overwrite and append
- redis.call("expire", key, expire)
- return redis.call("setrange", key, offset, new_data)
- else
- -- append and truncate
- local current_data = redis.call("get", key)
- new_data = current_data:sub(1, offset) .. new_data
- redis.call("set", key, new_data, "ex", expire)
- return redis.call("strlen", key)
- end
- EOS
-
- def available?
- true
- end
-
- def data(model)
- Gitlab::Redis::SharedState.with do |redis|
- redis.get(key(model))
- end
- end
-
- def set_data(model, new_data)
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(key(model), new_data, ex: CHUNK_REDIS_TTL)
- end
- end
-
- def append_data(model, new_data, offset)
- Gitlab::Redis::SharedState.with do |redis|
- redis.eval(LUA_APPEND_CHUNK, keys: [key(model)], argv: [new_data, offset])
- end
- end
-
- def size(model)
- Gitlab::Redis::SharedState.with do |redis|
- redis.strlen(key(model))
- end
- end
-
- def delete_data(model)
- delete_keys([[model.build_id, model.chunk_index]])
- end
-
- def keys(relation)
- relation.pluck(:build_id, :chunk_index)
- end
-
- def delete_keys(keys)
- return if keys.empty?
-
- keys = keys.map { |key| key_raw(*key) }
-
- Gitlab::Redis::SharedState.with do |redis|
- # https://gitlab.com/gitlab-org/gitlab/-/issues/224171
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(keys)
- end
- end
- end
-
+ class Redis < RedisBase
private
- def key(model)
- key_raw(model.build_id, model.chunk_index)
- end
-
- def key_raw(build_id, chunk_index)
- "gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
+ def with_redis
+ Gitlab::Redis::SharedState.with { |redis| yield(redis) }
end
end
end
diff --git a/app/models/ci/build_trace_chunks/redis_base.rb b/app/models/ci/build_trace_chunks/redis_base.rb
new file mode 100644
index 00000000000..3b7a844d122
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/redis_base.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Ci
+ module BuildTraceChunks
+ class RedisBase
+ CHUNK_REDIS_TTL = 1.week
+ LUA_APPEND_CHUNK = <<~EOS
+ local key, new_data, offset = KEYS[1], ARGV[1], ARGV[2]
+ local length = new_data:len()
+ local expire = #{CHUNK_REDIS_TTL.seconds}
+ local current_size = redis.call("strlen", key)
+ offset = tonumber(offset)
+
+ if offset == 0 then
+ -- overwrite everything
+ redis.call("set", key, new_data, "ex", expire)
+ return redis.call("strlen", key)
+ elseif offset > current_size then
+ -- offset range violation
+ return -1
+ elseif offset + length >= current_size then
+ -- efficiently append or overwrite and append
+ redis.call("expire", key, expire)
+ return redis.call("setrange", key, offset, new_data)
+ else
+ -- append and truncate
+ local current_data = redis.call("get", key)
+ new_data = current_data:sub(1, offset) .. new_data
+ redis.call("set", key, new_data, "ex", expire)
+ return redis.call("strlen", key)
+ end
+ EOS
+
+ def data(model)
+ with_redis do |redis|
+ redis.get(key(model))
+ end
+ end
+
+ def set_data(model, new_data)
+ with_redis do |redis|
+ redis.set(key(model), new_data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def append_data(model, new_data, offset)
+ with_redis do |redis|
+ redis.eval(LUA_APPEND_CHUNK, keys: [key(model)], argv: [new_data, offset])
+ end
+ end
+
+ def size(model)
+ with_redis do |redis|
+ redis.strlen(key(model))
+ end
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ return if keys.empty?
+
+ keys = keys.map { |key| key_raw(*key) }
+
+ with_redis do |redis|
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/224171
+ Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
+ redis.del(keys)
+ end
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/redis_trace_chunks.rb b/app/models/ci/build_trace_chunks/redis_trace_chunks.rb
new file mode 100644
index 00000000000..06e315b0aaf
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/redis_trace_chunks.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ module BuildTraceChunks
+ class RedisTraceChunks < RedisBase
+ private
+
+ def with_redis
+ Gitlab::Redis::TraceChunks.with { |redis| yield(redis) }
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb
index 5091e3ff04a..036f611a61c 100644
--- a/app/models/ci/build_trace_section.rb
+++ b/app/models/ci/build_trace_section.rb
@@ -4,11 +4,14 @@ module Ci
class BuildTraceSection < ApplicationRecord
extend SuppressCompositePrimaryKeyWarning
extend Gitlab::Ci::Model
+ include IgnorableColumns
belongs_to :build, class_name: 'Ci::Build'
belongs_to :project
belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName'
validates :section_name, :build, :project, presence: true, allow_blank: false
+
+ ignore_column :build_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 5248a80f710..6a7a2b3f6bd 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -18,7 +18,6 @@ module Ci
ACCESSIBILITY_REPORT_FILE_TYPES = %w[accessibility].freeze
NON_ERASABLE_FILE_TYPES = %w[trace].freeze
TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze
- UNSUPPORTED_FILE_TYPES = %i[license_management].freeze
SAST_REPORT_TYPES = %w[sast].freeze
SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze
DEFAULT_FILE_NAMES = {
@@ -35,7 +34,6 @@ module Ci
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json',
- license_management: 'gl-license-management-report.json',
license_scanning: 'gl-license-scanning-report.json',
performance: 'performance.json',
browser_performance: 'browser-performance.json',
@@ -45,7 +43,7 @@ module Ci
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
- cluster_applications: 'gl-cluster-applications.json',
+ cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/333441
requirements: 'requirements.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json',
api_fuzzing: 'gl-api-fuzzing-report.json'
@@ -74,7 +72,6 @@ module Ci
dependency_scanning: :raw,
container_scanning: :raw,
dast: :raw,
- license_management: :raw,
license_scanning: :raw,
# All these file formats use `raw` as we need to store them uncompressed
@@ -102,7 +99,6 @@ module Ci
dependency_scanning
dotenv
junit
- license_management
license_scanning
lsif
metrics
@@ -124,7 +120,6 @@ module Ci
mount_file_store_uploader JobArtifactUploader
validates :file_format, presence: true, unless: :trace?, on: :create
- validate :validate_supported_file_format!, on: :create
validate :validate_file_format!, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
@@ -199,8 +194,7 @@ module Ci
container_scanning: 7, ## EE-specific
dast: 8, ## EE-specific
codequality: 9, ## EE-specific
- license_management: 10, ## EE-specific
- license_scanning: 101, ## EE-specific till 13.0
+ license_scanning: 101, ## EE-specific
performance: 11, ## EE-specific till 13.2
metrics: 12, ## EE-specific
metrics_referee: 13, ## runner referees
@@ -233,14 +227,6 @@ module Ci
hashed_path: 2
}
- def validate_supported_file_format!
- return if Feature.disabled?(:drop_license_management_artifact, project, default_enabled: true)
-
- if UNSUPPORTED_FILE_TYPES.include?(self.file_type&.to_sym)
- errors.add(:base, _("File format is no longer supported"))
- end
- end
-
def validate_file_format!
unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym
errors.add(:base, _('Invalid file format with specified file type'))
diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb
new file mode 100644
index 00000000000..283ad4a190d
--- /dev/null
+++ b/app/models/ci/job_token/project_scope_link.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# The connection between a source project (which defines the job token scope)
+# and a target project which is the one allowed to be accessed by the job token.
+
+module Ci
+ module JobToken
+ class ProjectScopeLink < ApplicationRecord
+ self.table_name = 'ci_job_token_project_scope_links'
+
+ belongs_to :source_project, class_name: 'Project'
+ belongs_to :target_project, class_name: 'Project'
+ belongs_to :added_by, class_name: 'User'
+
+ scope :from_project, ->(project) { where(source_project: project) }
+ scope :to_project, ->(project) { where(target_project: project) }
+
+ validates :source_project, presence: true
+ validates :target_project, presence: true
+ validate :not_self_referential_link
+
+ private
+
+ def not_self_referential_link
+ return unless source_project && target_project
+
+ if source_project == target_project
+ self.errors.add(:target_project, _("can't be the same as the source project"))
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
new file mode 100644
index 00000000000..42cfdc21d66
--- /dev/null
+++ b/app/models/ci/job_token/scope.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# This model represents the surface where a CI_JOB_TOKEN can be used.
+# A Scope is initialized with the project that the job token belongs to,
+# and indicates what are all the other projects that the token could access.
+#
+# By default a job token can only access its own project, which is the same
+# project that defines the scope.
+# By adding ScopeLinks to the scope we can allow other projects to be accessed
+# by the job token. This works as an allowlist of projects for a job token.
+#
+# If a project is not included in the scope we should not allow the job user
+# to access it since operations using CI_JOB_TOKEN should be considered untrusted.
+
+module Ci
+ module JobToken
+ class Scope
+ attr_reader :source_project
+
+ def initialize(project)
+ @source_project = project
+ end
+
+ def includes?(target_project)
+ # if the setting is disabled any project is considered to be in scope.
+ return true unless source_project.ci_job_token_scope_enabled?
+
+ target_project.id == source_project.id ||
+ Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists?
+ end
+
+ def all_projects
+ Project.from_union([
+ Project.id_in(source_project),
+ Project.where_exists(
+ Ci::JobToken::ProjectScopeLink
+ .from_project(source_project)
+ .where('projects.id = ci_job_token_project_scope_links.target_project_id'))
+ ], remove_duplicates: false)
+ end
+ end
+ end
+end
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
new file mode 100644
index 00000000000..b9a8a44bd6b
--- /dev/null
+++ b/app/models/ci/pending_build.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Ci
+ class PendingBuild < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ belongs_to :project
+ belongs_to :build, class_name: 'Ci::Build'
+
+ def self.upsert_from_build!(build)
+ entry = self.new(build: build, project: build.project, protected: build.protected?)
+
+ entry.validate!
+
+ self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f0a2c074584..ae06bea5a02 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -644,6 +644,10 @@ module Ci
end
end
+ def update_builds_coverage
+ builds.with_coverage_regex.without_coverage.each(&:update_coverage)
+ end
+
def batch_lookup_report_artifact_for_file_type(file_type)
latest_report_artifacts
.values_at(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s))
@@ -660,15 +664,9 @@ module Ci
# Return a hash of file type => array of 1 job artifact
def latest_report_artifacts
::Gitlab::SafeRequestStore.fetch("pipeline:#{self.id}:latest_report_artifacts") do
- # Note we use read_attribute(:project_id) to read the project
- # ID instead of self.project_id. The latter appears to load
- # the Project model. This extra filter doesn't appear to
- # affect query plan but included to ensure we don't leak the
- # wrong informaiton.
::Ci::JobArtifact.where(
id: job_artifacts.with_reports
.select('max(ci_job_artifacts.id) as id')
- .where(project_id: self.read_attribute(:project_id))
.group(:file_type)
)
.preload(:job)
@@ -928,6 +926,12 @@ module Ci
Ci::Build.latest.where(pipeline: self_and_descendants)
end
+ def environments_in_self_and_descendants
+ environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id')
+
+ Environment.where(id: environment_ids)
+ end
+
# Without using `unscoped`, caller scope is also included into the query.
# Using `unscoped` here will be redundant after Rails 6.1
def self_and_descendants
@@ -1252,6 +1256,10 @@ module Ci
end
end
+ def build_matchers
+ self.builds.build_matchers(project)
+ end
+
private
def add_message(severity, content)
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 9e5d517c1fe..effe2d95a99 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -3,6 +3,7 @@
module Ci
class PipelineSchedule < ApplicationRecord
extend Gitlab::Ci::Model
+ extend ::Gitlab::Utils::Override
include Importable
include StripAttribute
include CronSchedulable
@@ -55,6 +56,17 @@ module Ci
variables&.map(&:to_runner_variable) || []
end
+ override :set_next_run_at
+ def set_next_run_at
+ self.next_run_at = ::Ci::PipelineSchedules::CalculateNextRunService # rubocop: disable CodeReuse/ServiceClass
+ .new(project)
+ .execute(self, fallback_method: method(:calculate_next_run_at))
+ end
+
+ def daily_limit
+ project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers)
+ end
+
private
def worker_cron_expression
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 15c57550159..e2f257eab25 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -120,7 +120,7 @@ module Ci
raise NotImplementedError
end
- def instantized_environment
+ def persisted_environment
raise NotImplementedError
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 8c877c2b818..71110ef0696 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -10,6 +10,8 @@ module Ci
include TokenAuthenticatable
include IgnorableColumns
include FeatureGate
+ include Gitlab::Utils::StrongMemoize
+ include TaggableQueries
add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
@@ -58,6 +60,7 @@ module Ci
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', online_contact_time_deadline) }
+ scope :recent, -> { where('ci_runners.created_at > :date OR ci_runners.contacted_at > :date', date: 3.months.ago) }
# The following query using negation is cheaper than using `contacted_at <= ?`
# because there are less runners online than have been created. The
# resulting query is quickly finding online ones and then uses the regular
@@ -131,6 +134,8 @@ module Ci
end
scope :order_contacted_at_asc, -> { order(contacted_at: :asc) }
+ scope :order_contacted_at_desc, -> { order(contacted_at: :desc) }
+ scope :order_created_at_asc, -> { order(created_at: :asc) }
scope :order_created_at_desc, -> { order(created_at: :desc) }
scope :with_tags, -> { preload(:tags) }
@@ -161,20 +166,17 @@ module Ci
numericality: { greater_than_or_equal_to: 0.0,
message: 'needs to be non-negative' }
+ validates :config, json_schema: { filename: 'ci_runner_config' }
+
# Searches for runners matching the given query.
#
- # This method uses ILIKE on PostgreSQL.
- #
- # This method performs a *partial* match on tokens, thus a query for "a"
- # will match any runner where the token contains the letter "a". As a result
- # you should *not* use this method for non-admin purposes as otherwise users
- # might be able to query a list of all runners.
+ # This method uses ILIKE on PostgreSQL for the description field and performs a full match on tokens.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def self.search(query)
- fuzzy_search(query, [:token, :description])
+ where(token: query).or(fuzzy_search(query, [:description]))
end
def self.online_contact_time_deadline
@@ -190,13 +192,54 @@ module Ci
end
def self.order_by(order)
- if order == 'contacted_asc'
+ case order
+ when 'contacted_asc'
order_contacted_at_asc
+ when 'contacted_desc'
+ order_contacted_at_desc
+ when 'created_at_asc'
+ order_created_at_asc
else
order_created_at_desc
end
end
+ def self.runner_matchers
+ unique_params = [
+ :runner_type,
+ :public_projects_minutes_cost_factor,
+ :private_projects_minutes_cost_factor,
+ :run_untagged,
+ :access_level,
+ Arel.sql("(#{arel_tag_names_array.to_sql})")
+ ]
+
+ # we use distinct to de-duplicate data
+ distinct.pluck(*unique_params).map do |values|
+ Gitlab::Ci::Matching::RunnerMatcher.new({
+ runner_type: values[0],
+ public_projects_minutes_cost_factor: values[1],
+ private_projects_minutes_cost_factor: values[2],
+ run_untagged: values[3],
+ access_level: values[4],
+ tag_list: values[5]
+ })
+ end
+ end
+
+ def runner_matcher
+ strong_memoize(:runner_matcher) do
+ Gitlab::Ci::Matching::RunnerMatcher.new({
+ runner_type: runner_type,
+ public_projects_minutes_cost_factor: public_projects_minutes_cost_factor,
+ private_projects_minutes_cost_factor: private_projects_minutes_cost_factor,
+ run_untagged: run_untagged,
+ access_level: access_level,
+ tag_list: tag_list
+ })
+ end
+ end
+
def assign_to(project, current_user = nil)
if instance_type?
self.runner_type = :project_type
@@ -298,6 +341,14 @@ module Ci
end
def tick_runner_queue
+ ##
+ # We only stick a runner to primary database to be able to detect the
+ # replication lag in `EE::Ci::RegisterJobService#execute`. The
+ # intention here is not to execute `Ci::RegisterJobService#execute` on
+ # the primary database.
+ #
+ ::Gitlab::Database::LoadBalancing::Sticking.stick(:runner, id)
+
SecureRandom.hex.tap do |new_update|
::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update,
expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true)
@@ -315,21 +366,24 @@ module Ci
end
def heartbeat(values)
- values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {}
- values[:contacted_at] = Time.current
+ ##
+ # We can safely ignore writes performed by a runner heartbeat. We do
+ # not want to upgrade database connection proxy to use the primary
+ # database after heartbeat write happens.
+ #
+ ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
+ values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config) || {}
+ values[:contacted_at] = Time.current
- cache_attributes(values)
+ cache_attributes(values)
- # We save data without validation, it will always change due to `contacted_at`
- self.update_columns(values) if persist_cached_data?
+ # We save data without validation, it will always change due to `contacted_at`
+ self.update_columns(values) if persist_cached_data?
+ end
end
def pick_build!(build)
- if Feature.enabled?(:ci_reduce_queries_when_ticking_runner_queue, self, default_enabled: :yaml)
- tick_runner_queue if matches_build?(build)
- else
- tick_runner_queue if can_pick?(build)
- end
+ tick_runner_queue if matches_build?(build)
end
def uncached_contacted_at
@@ -395,13 +449,7 @@ module Ci
end
def matches_build?(build)
- return false if self.ref_protected? && !build.protected?
-
- accepting_tags?(build)
- end
-
- def accepting_tags?(build)
- (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
+ runner_matcher.matches?(build.build_matcher)
end
end
end
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index f819dda207d..41a4c9012ff 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -7,6 +7,7 @@ module Ci
self.limit_name = 'ci_registered_group_runners'
self.limit_scope = :group
+ self.limit_relation = :recent_runners
self.limit_feature_flag = :ci_runner_limits
belongs_to :runner, inverse_of: :runner_namespaces
@@ -16,6 +17,10 @@ module Ci
validates :runner_id, uniqueness: { scope: :namespace_id }
validate :group_runner_type
+ def recent_runners
+ ::Ci::Runner.belonging_to_group(namespace_id).recent
+ end
+
private
def group_runner_type
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index c26b8183b52..af2595ce4af 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -7,11 +7,16 @@ module Ci
self.limit_name = 'ci_registered_project_runners'
self.limit_scope = :project
+ self.limit_relation = :recent_runners
self.limit_feature_flag = :ci_runner_limits
belongs_to :runner, inverse_of: :runner_projects
belongs_to :project, inverse_of: :runner_projects
+ def recent_runners
+ ::Ci::Runner.belonging_to_project(project_id).recent
+ end
+
validates :runner_id, uniqueness: { scope: :project_id }
end
end
diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb
new file mode 100644
index 00000000000..9446cfa05da
--- /dev/null
+++ b/app/models/ci/running_build.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Ci
+ class RunningBuild < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ belongs_to :project
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :runner, class_name: 'Ci::Runner'
+
+ enum runner_type: ::Ci::Runner.runner_types
+
+ def self.upsert_shared_runner_build!(build)
+ unless build.shared_runner_build?
+ raise ArgumentError, 'build has not been picked by a shared runner'
+ end
+
+ entry = self.new(build: build,
+ project: build.project,
+ runner: build.runner,
+ runner_type: build.runner.runner_type)
+
+ entry.validate!
+
+ self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
+ end
+ end
+end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index ef920b2d589..d00066b778d 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -7,6 +7,9 @@ module Ci
include Ci::HasStatus
include Gitlab::OptimisticLocking
include Presentable
+ include IgnorableColumns
+
+ ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
enum status: Ci::HasStatus::STATUSES_ENUM
diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb
deleted file mode 100644
index 91aa422b859..00000000000
--- a/app/models/clusters/applications/fluentd.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class Fluentd < ApplicationRecord
- VERSION = '2.4.0'
- CILIUM_CONTAINER_NAME = 'cilium-monitor'
-
- self.table_name = 'clusters_applications_fluentd'
-
- include ::Clusters::Concerns::ApplicationCore
- include ::Clusters::Concerns::ApplicationStatus
- include ::Clusters::Concerns::ApplicationVersion
- include ::Clusters::Concerns::ApplicationData
-
- default_value_for :version, VERSION
- default_value_for :port, 514
- default_value_for :protocol, :tcp
-
- enum protocol: { tcp: 0, udp: 1 }
-
- validate :has_at_least_one_log_enabled?
-
- def chart
- 'fluentd/fluentd'
- end
-
- def repository
- 'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
- end
-
- def install_command
- helm_command_module::InstallCommand.new(
- name: 'fluentd',
- repository: repository,
- version: VERSION,
- rbac: cluster.platform_kubernetes_rbac?,
- chart: chart,
- files: files
- )
- end
-
- def values
- content_values.to_yaml
- end
-
- private
-
- def has_at_least_one_log_enabled?
- if !waf_log_enabled && !cilium_log_enabled
- errors.add(:base, _("At least one logging option is required to be enabled"))
- end
- end
-
- def content_values
- YAML.load_file(chart_values_file).deep_merge!(specification)
- end
-
- def specification
- {
- "configMaps" => {
- "output.conf" => output_configuration_content,
- "general.conf" => general_configuration_content
- }
- }
- end
-
- def output_configuration_content
- <<~EOF
- <match kubernetes.**>
- @type remote_syslog
- @id out_kube_remote_syslog
- host #{host}
- port #{port}
- program fluentd
- hostname ${kubernetes_host}
- protocol #{protocol}
- packet_size 131072
- <buffer kubernetes_host>
- </buffer>
- <format>
- @type ltsv
- </format>
- </match>
- EOF
- end
-
- def general_configuration_content
- <<~EOF
- <match fluent.**>
- @type null
- </match>
- <source>
- @type http
- port 9880
- bind 0.0.0.0
- </source>
- <source>
- @type tail
- @id in_tail_container_logs
- path #{path_to_logs}
- pos_file /var/log/fluentd-containers.log.pos
- tag kubernetes.*
- read_from_head true
- <parse>
- @type json
- time_format %Y-%m-%dT%H:%M:%S.%NZ
- </parse>
- </source>
- EOF
- end
-
- def path_to_logs
- path = []
- path << "/var/log/containers/*#{Ingress::MODSECURITY_LOG_CONTAINER_NAME}*.log" if waf_log_enabled
- path << "/var/log/containers/*#{CILIUM_CONTAINER_NAME}*.log" if cilium_log_enabled
- path.join(',')
- end
- end
- end
-end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index e7d4d737b8e..3a8c314efe4 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -7,10 +7,6 @@ module Clusters
class Ingress < ApplicationRecord
VERSION = '1.40.2'
INGRESS_CONTAINER_NAME = 'nginx-ingress-controller'
- MODSECURITY_LOG_CONTAINER_NAME = 'modsecurity-log'
- MODSECURITY_MODE_LOGGING = "DetectionOnly"
- MODSECURITY_MODE_BLOCKING = "On"
- MODSECURITY_OWASP_RULES_FILE = "/etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf"
self.table_name = 'clusters_applications_ingress'
@@ -20,22 +16,18 @@ module Clusters
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
include UsageStatistics
+ include IgnorableColumns
default_value_for :ingress_type, :nginx
- default_value_for :modsecurity_enabled, true
default_value_for :version, VERSION
- default_value_for :modsecurity_mode, :logging
+
+ ignore_column :modsecurity_enabled, remove_with: '14.2', remove_after: '2021-07-22'
+ ignore_column :modsecurity_mode, remove_with: '14.2', remove_after: '2021-07-22'
enum ingress_type: {
nginx: 1
}
- enum modsecurity_mode: { logging: 0, blocking: 1 }
-
- scope :modsecurity_not_installed, -> { where(modsecurity_enabled: nil) }
- scope :modsecurity_enabled, -> { where(modsecurity_enabled: true) }
- scope :modsecurity_disabled, -> { where(modsecurity_enabled: false) }
-
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
@@ -92,96 +84,13 @@ module Clusters
private
- def specification
- return {} unless modsecurity_enabled
-
- {
- "controller" => {
- "config" => {
- "enable-modsecurity" => "true",
- "enable-owasp-modsecurity-crs" => "false",
- "modsecurity-snippet" => modsecurity_snippet_content,
- "modsecurity.conf" => modsecurity_config_content
- },
- "extraContainers" => [
- {
- "name" => MODSECURITY_LOG_CONTAINER_NAME,
- "image" => "busybox",
- "args" => [
- "/bin/sh",
- "-c",
- "tail -F /var/log/modsec/audit.log"
- ],
- "volumeMounts" => [
- {
- "name" => "modsecurity-log-volume",
- "mountPath" => "/var/log/modsec",
- "readOnly" => true
- }
- ],
- "livenessProbe" => {
- "exec" => {
- "command" => [
- "ls",
- "/var/log/modsec/audit.log"
- ]
- }
- }
- }
- ],
- "extraVolumeMounts" => [
- {
- "name" => "modsecurity-template-volume",
- "mountPath" => "/etc/nginx/modsecurity/modsecurity.conf",
- "subPath" => "modsecurity.conf"
- },
- {
- "name" => "modsecurity-log-volume",
- "mountPath" => "/var/log/modsec"
- }
- ],
- "extraVolumes" => [
- {
- "name" => "modsecurity-template-volume",
- "configMap" => {
- "name" => "ingress-#{INGRESS_CONTAINER_NAME}",
- "items" => [
- {
- "key" => "modsecurity.conf",
- "path" => "modsecurity.conf"
- }
- ]
- }
- },
- {
- "name" => "modsecurity-log-volume",
- "emptyDir" => {}
- }
- ]
- }
- }
- end
-
- def modsecurity_config_content
- File.read(modsecurity_config_file_path)
- end
-
- def modsecurity_config_file_path
- Rails.root.join('vendor', 'ingress', 'modsecurity.conf')
- end
-
def content_values
- YAML.load_file(chart_values_file).deep_merge!(specification)
+ YAML.load_file(chart_values_file)
end
def application_jupyter_installed?
cluster.application_jupyter&.installed?
end
-
- def modsecurity_snippet_content
- sec_rule_engine = logging? ? MODSECURITY_MODE_LOGGING : MODSECURITY_MODE_BLOCKING
- "SecRuleEngine #{sec_rule_engine}\nInclude #{MODSECURITY_OWASP_RULES_FILE}"
- end
end
end
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 6867d7b6934..0e7cbb35e47 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -141,13 +141,13 @@ module Clusters
end
def install_knative_metrics
- return [] unless cluster.application_prometheus_available?
+ return [] unless cluster.application_prometheus&.available?
[Gitlab::Kubernetes::KubectlCmd.apply_file(METRICS_CONFIG)]
end
def delete_knative_istio_metrics
- return [] unless cluster.application_prometheus_available?
+ return [] unless cluster.application_prometheus&.available?
[Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)]
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index e8d56072b89..49840e3a2e7 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.28.0'
+ VERSION = '0.29.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 4877ced795c..2fff0a69a26 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -21,7 +21,6 @@ module Clusters
Clusters::Applications::Jupyter.application_name => Clusters::Applications::Jupyter,
Clusters::Applications::Knative.application_name => Clusters::Applications::Knative,
Clusters::Applications::ElasticStack.application_name => Clusters::Applications::ElasticStack,
- Clusters::Applications::Fluentd.application_name => Clusters::Applications::Fluentd,
Clusters::Applications::Cilium.application_name => Clusters::Applications::Cilium
}.freeze
DEFAULT_ENVIRONMENT = '*'
@@ -68,7 +67,6 @@ module Clusters
has_one_cluster_application :jupyter
has_one_cluster_application :knative
has_one_cluster_application :elastic_stack
- has_one_cluster_application :fluentd
has_one_cluster_application :cilium
has_many :kubernetes_namespaces
@@ -104,8 +102,8 @@ module Clusters
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
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 :available?, to: :integration_prometheus, 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
@@ -138,11 +136,10 @@ module Clusters
scope :gcp_installed, -> { gcp_provided.joins(:provider_gcp).merge(Clusters::Providers::Gcp.with_status(:created)) }
scope :aws_installed, -> { aws_provided.joins(:provider_aws).merge(Clusters::Providers::Aws.with_status(:created)) }
- scope :with_enabled_modsecurity, -> { joins(:application_ingress).merge(::Clusters::Applications::Ingress.modsecurity_enabled) }
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
- scope :preload_elasticstack, -> { preload(:application_elastic_stack) }
+ scope :preload_elasticstack, -> { preload(:integration_elastic_stack) }
scope :preload_environments, -> { preload(:environments) }
scope :managed, -> { where(managed: true) }
@@ -171,18 +168,16 @@ module Clusters
state_machine :cleanup_status, initial: :cleanup_not_started do
state :cleanup_not_started, value: 1
- state :cleanup_uninstalling_applications, value: 2
state :cleanup_removing_project_namespaces, value: 3
state :cleanup_removing_service_account, value: 4
state :cleanup_errored, value: 5
event :start_cleanup do |cluster|
- transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications
+ transition [:cleanup_not_started, :cleanup_errored] => :cleanup_removing_project_namespaces
end
event :continue_cleanup do
transition(
- cleanup_uninstalling_applications: :cleanup_removing_project_namespaces,
cleanup_removing_project_namespaces: :cleanup_removing_service_account)
end
@@ -195,13 +190,7 @@ module Clusters
cluster.cleanup_status_reason = status_reason if status_reason
end
- after_transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications do |cluster|
- cluster.run_after_commit do
- Clusters::Cleanup::AppWorker.perform_async(cluster.id)
- end
- end
-
- after_transition cleanup_uninstalling_applications: :cleanup_removing_project_namespaces do |cluster|
+ after_transition [:cleanup_not_started, :cleanup_errored] => :cleanup_removing_project_namespaces do |cluster|
cluster.run_after_commit do
Clusters::Cleanup::ProjectNamespaceWorker.perform_async(cluster.id)
end
@@ -325,7 +314,7 @@ module Clusters
end
def elastic_stack_adapter
- application_elastic_stack || integration_elastic_stack
+ integration_elastic_stack
end
def elasticsearch_client
@@ -333,11 +322,7 @@ module Clusters
end
def elastic_stack_available?
- if application_elastic_stack_available? || integration_elastic_stack_available?
- true
- else
- false
- end
+ !!integration_elastic_stack_available?
end
def kubernetes_namespace_for(environment, deployable: environment.last_deployable)
@@ -391,12 +376,8 @@ module Clusters
end
end
- def application_prometheus_available?
- integration_prometheus&.available? || application_prometheus&.available?
- end
-
def prometheus_adapter
- integration_prometheus || application_prometheus
+ integration_prometheus
end
private
diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb
index 125783e6ee1..162a1a3290d 100644
--- a/app/models/clusters/clusters_hierarchy.rb
+++ b/app/models/clusters/clusters_hierarchy.rb
@@ -4,9 +4,8 @@ module Clusters
class ClustersHierarchy
DEPTH_COLUMN = :depth
- def initialize(clusterable, include_management_project: true)
+ def initialize(clusterable)
@clusterable = clusterable
- @include_management_project = include_management_project
end
# Returns clusters in order from deepest to highest group
@@ -25,7 +24,7 @@ module Clusters
private
- attr_reader :clusterable, :include_management_project
+ attr_reader :clusterable
def recursive_cte
cte = Gitlab::SQL::RecursiveCTE.new(:clusters_cte)
@@ -39,7 +38,7 @@ module Clusters
raise ArgumentError, "unknown type for #{clusterable}"
end
- if clusterable.is_a?(::Project) && include_management_project
+ if clusterable.is_a?(::Project)
cte << same_namespace_management_clusters_query
end
@@ -71,7 +70,7 @@ module Clusters
# Only applicable if the clusterable is a project (most especially when
# requesting project.deployment_platform).
def depth_order_clause
- return { DEPTH_COLUMN => :asc } unless clusterable.is_a?(::Project) && include_management_project
+ return { DEPTH_COLUMN => :asc } unless clusterable.is_a?(::Project)
order = <<~SQL
(CASE clusters.management_project_id
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 09e43bb8f20..a1ed5eb9ab9 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -15,8 +15,6 @@ class Commit
include ActsAsPaginatedDiff
include CacheMarkdownField
- attr_mentionable :safe_message, pipeline: :single_line
-
participant :author
participant :committer
participant :notes_with_associations
@@ -35,10 +33,20 @@ class Commit
# Used by GFM to match and present link extensions on node texts and hrefs.
LINK_EXTENSION_PATTERN = /(patch)/.freeze
+ DEFAULT_MAX_DIFF_LINES_SETTING = 50_000
+ DEFAULT_MAX_DIFF_FILES_SETTING = 1_000
+ MAX_DIFF_LINES_SETTING_UPPER_BOUND = 100_000
+ MAX_DIFF_FILES_SETTING_UPPER_BOUND = 3_000
+ DIFF_SAFE_LIMIT_FACTOR = 10
+
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :full_title, pipeline: :single_line, limit: 1.kilobyte
cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte
+ # Share the cache used by the markdown fields
+ attr_mentionable :title, pipeline: :single_line
+ attr_mentionable :description, pipeline: :commit_description, limit: 1.megabyte
+
class << self
def decorate(commits, container)
commits.map do |commit|
@@ -76,20 +84,24 @@ class Commit
end
def diff_safe_lines(project: nil)
- Gitlab::Git::DiffCollection.default_limits(project: project)[:max_lines]
+ diff_safe_max_lines(project: project)
end
- def diff_hard_limit_files(project: nil)
+ def diff_max_files(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
3000
+ elsif Feature.enabled?(:configurable_diff_limits, project)
+ Gitlab::CurrentSettings.diff_max_files
else
1000
end
end
- def diff_hard_limit_lines(project: nil)
+ def diff_max_lines(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
100000
+ elsif Feature.enabled?(:configurable_diff_limits, project)
+ Gitlab::CurrentSettings.diff_max_lines
else
50000
end
@@ -97,11 +109,19 @@ class Commit
def max_diff_options(project: nil)
{
- max_files: diff_hard_limit_files(project: project),
- max_lines: diff_hard_limit_lines(project: project)
+ max_files: diff_max_files(project: project),
+ max_lines: diff_max_lines(project: project)
}
end
+ def diff_safe_max_files(project: nil)
+ diff_max_files(project: project) / DIFF_SAFE_LIMIT_FACTOR
+ end
+
+ def diff_safe_max_lines(project: nil)
+ diff_max_lines(project: project) / DIFF_SAFE_LIMIT_FACTOR
+ end
+
def from_hash(hash, container)
raw_commit = Gitlab::Git::Commit.new(container.repository.raw, hash)
new(raw_commit, container)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c5ba19438cd..2db606898b9 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -57,6 +57,9 @@ class CommitStatus < ApplicationRecord
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :eager_load_pipeline, -> { eager_load(:pipeline, project: { namespace: :route }) }
scope :with_pipeline, -> { joins(:pipeline) }
+ scope :updated_before, ->(lookback:, timeout:) {
+ where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
+ }
scope :for_project_paths, -> (paths) do
where(project: Project.where_full_path_in(Array(paths)))
@@ -174,8 +177,11 @@ class CommitStatus < ApplicationRecord
next if commit_status.processed?
next unless commit_status.project
+ last_arg = transition.args.last
+ transition_options = last_arg.is_a?(Hash) && last_arg.extractable_options? ? last_arg : {}
+
commit_status.run_after_commit do
- PipelineProcessWorker.perform_async(pipeline_id)
+ PipelineProcessWorker.perform_async(pipeline_id) unless transition_options[:skip_pipeline_processing]
ExpireJobCacheWorker.perform_async(id)
end
end
diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb
index 3748e77e933..908f0b6a7e2 100644
--- a/app/models/concerns/bulk_insert_safe.rb
+++ b/app/models/concerns/bulk_insert_safe.rb
@@ -141,6 +141,12 @@ module BulkInsertSafe
raise ArgumentError, "returns needs to be :ids or nil"
end
+ # Handle insertions for tables with a composite primary key
+ primary_keys = connection.schema_cache.primary_keys(table_name)
+ if unique_by.blank? && primary_key != primary_keys
+ unique_by = primary_keys
+ end
+
transaction do
items.each_slice(batch_size).flat_map do |item_batch|
attributes = _bulk_insert_item_attributes(
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index a5cf947ba07..101bff32dfe 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -27,7 +27,7 @@ module CacheMarkdownField
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError, "Unknown field: #{field.inspect}" unless
- cached_markdown_fields.markdown_fields.include?(field)
+ cached_markdown_fields.key?(field)
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
@@ -100,7 +100,7 @@ module CacheMarkdownField
def cached_html_for(markdown_field)
raise ArgumentError, "Unknown field: #{markdown_field}" unless
- cached_markdown_fields.markdown_fields.include?(markdown_field)
+ cached_markdown_fields.key?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -108,7 +108,7 @@ module CacheMarkdownField
# Updates the markdown cache if necessary, then returns the field
# Unlike `cached_html_for` it returns `nil` if the field does not exist
def updated_cached_html_for(markdown_field)
- return unless cached_markdown_fields.markdown_fields.include?(markdown_field)
+ return unless cached_markdown_fields.key?(markdown_field)
if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field))
# Invalidated due to Markdown content change
@@ -157,6 +157,9 @@ module CacheMarkdownField
end
def store_mentions!
+ # We can only store mentions if the mentionable is a database object
+ return unless self.is_a?(ApplicationRecord)
+
refs = all_references(self.author)
references = {}
diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb
index beb3a09c119..48605ecc3d7 100644
--- a/app/models/concerns/cron_schedulable.rb
+++ b/app/models/concerns/cron_schedulable.rb
@@ -4,23 +4,28 @@ module CronSchedulable
extend ActiveSupport::Concern
include Schedulable
+ def set_next_run_at
+ self.next_run_at = calculate_next_run_at
+ end
+
+ private
+
##
# 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
+ def calculate_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
+ 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
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index 02f7711e927..b6245e29746 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -10,10 +10,6 @@ module DeploymentPlatform
private
- def cluster_management_project_enabled?
- Feature.enabled?(:cluster_management_project, self, default_enabled: true)
- end
-
def find_deployment_platform(environment)
find_platform_kubernetes_with_cte(environment) ||
find_instance_cluster_platform_kubernetes(environment: environment)
@@ -21,13 +17,13 @@ module DeploymentPlatform
def find_platform_kubernetes_with_cte(environment)
if environment
- ::Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?)
+ ::Clusters::ClustersHierarchy.new(self)
.base_and_ancestors
.enabled
.on_environment(environment, relevant_only: true)
.first&.platform_kubernetes
else
- Clusters::ClustersHierarchy.new(self, include_management_project: cluster_management_project_enabled?).base_and_ancestors
+ Clusters::ClustersHierarchy.new(self).base_and_ancestors
.enabled.default_environment
.first&.platform_kubernetes
end
diff --git a/app/models/concerns/enum_with_nil.rb b/app/models/concerns/enum_with_nil.rb
index 6d0a21cf070..c66942025d7 100644
--- a/app/models/concerns/enum_with_nil.rb
+++ b/app/models/concerns/enum_with_nil.rb
@@ -11,14 +11,6 @@ module EnumWithNil
# override auto-defined methods only for the
# key which uses nil value
definitions.each do |name, values|
- next unless key_with_nil = values.key(nil)
-
- # E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
- # this overrides auto-generated method `unknown_failure?`
- define_method("#{key_with_nil}?") do
- self[name].nil?
- end
-
# E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
# this overrides auto-generated method `failure_reason`
define_method(name) do
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 2e368b12cb7..72788d15c0a 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -24,6 +24,7 @@ module Enums
project_deleted: 15,
ci_quota_exceeded: 16,
pipeline_loop_detected: 17,
+ no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
index 55360eb92e6..749d1ad65cd 100644
--- a/app/models/concerns/enums/vulnerability.rb
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -29,6 +29,14 @@ module Enums
critical: 7
}.with_indifferent_access.freeze
+ DETECTION_METHODS = {
+ gitlab_security_report: 0,
+ external_security_report: 1,
+ bug_bounty: 2,
+ code_review: 3,
+ security_audit: 4
+ }.with_indifferent_access.freeze
+
def self.confidence_levels
CONFIDENCE_LEVELS
end
@@ -40,6 +48,10 @@ module Enums
def self.severity_levels
SEVERITY_LEVELS
end
+
+ def self.detection_methods
+ DETECTION_METHODS
+ end
end
end
diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb
deleted file mode 100644
index 3af063438bf..00000000000
--- a/app/models/concerns/has_timelogs_report.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-module HasTimelogsReport
- extend ActiveSupport::Concern
- include Gitlab::Utils::StrongMemoize
-
- def timelogs(start_time, end_time)
- strong_memoize(:timelogs) { timelogs_for(start_time, end_time) }
- end
-
- def user_can_access_group_timelogs?(current_user)
- Ability.allowed?(current_user, :read_group_timelogs, self)
- end
-
- private
-
- def timelogs_for(start_time, end_time)
- Timelog.between_times(start_time, end_time).in_group(self)
- end
-end
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 468387115e5..4b4f9c0df84 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -12,10 +12,11 @@ module HasUserType
ghost: 5,
project_bot: 6,
migration_bot: 7,
- security_bot: 8
+ security_bot: 8,
+ automation_bot: 9
}.with_indifferent_access.freeze
- BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot].freeze
+ BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot].freeze
NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
diff --git a/app/models/concerns/services/data_fields.rb b/app/models/concerns/integrations/base_data_fields.rb
index fd56af449bc..3cedb90756f 100644
--- a/app/models/concerns/services/data_fields.rb
+++ b/app/models/concerns/integrations/base_data_fields.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
-module Services
- module DataFields
+module Integrations
+ module BaseDataFields
extend ActiveSupport::Concern
included do
- belongs_to :integration, inverse_of: self.name.underscore.to_sym, foreign_key: :service_id
+ # TODO: Once we rename the tables we can't rely on `table_name` anymore.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331953
+ belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :service_id
delegate :activated?, to: :integration, allow_nil: true
diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb
new file mode 100644
index 00000000000..e9aaaac8226
--- /dev/null
+++ b/app/models/concerns/integrations/has_data_fields.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Integrations
+ module HasDataFields
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Provide convenient accessor methods for data fields.
+ # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
+ def data_field(*args)
+ args.each do |arg|
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ unless method_defined?(arg)
+ def #{arg}
+ data_fields.send('#{arg}') || (properties && properties['#{arg}'])
+ end
+ end
+
+ def #{arg}=(value)
+ @old_data_fields ||= {}
+ @old_data_fields['#{arg}'] ||= #{arg} # set only on the first assignment, IOW we remember the original value only
+ data_fields.send('#{arg}=', value)
+ end
+
+ def #{arg}_touched?
+ @old_data_fields ||= {}
+ @old_data_fields.has_key?('#{arg}')
+ end
+
+ def #{arg}_changed?
+ #{arg}_touched? && @old_data_fields['#{arg}'] != #{arg}
+ end
+
+ def #{arg}_was
+ return unless #{arg}_touched?
+ return if data_fields.persisted? # arg_was does not work for attr_encrypted
+
+ legacy_properties_data['#{arg}']
+ end
+ RUBY
+ end
+ end
+ end
+
+ included do
+ has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
+ has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData'
+ has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData'
+
+ def data_fields
+ raise NotImplementedError
+ end
+
+ def data_fields_present?
+ data_fields.present?
+ rescue NotImplementedError
+ false
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/slack_mattermost/notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb
index 1a78cea5933..a919fc840fd 100644
--- a/app/models/project_services/slack_mattermost/notifier.rb
+++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
-module SlackMattermost
- module Notifier
+module Integrations
+ module SlackMattermostNotifier
private
def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
- notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
+ notifier = ::Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping(
message.pretext,
attachments: message.attachments,
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f5c70f10dc5..2d06247a486 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -101,20 +101,19 @@ module Issuable
scope :unassigned, -> do
where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
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.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
+ scope :assigned_to, ->(users) do
+ assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
+ condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ where(condition.arel.exists)
+ end
scope :not_assigned_to, ->(users) do
- assignees_table = Arel::Table.new("#{to_ability_name}_assignees")
- sql = assignees_table.project('true')
- .where(assignees_table[:user_id].in(users))
- .where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
- where(sql.exists.not)
+ assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
+
+ condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ where(condition.arel.exists.not)
end
+ # rubocop:enable GitlabSecurity/SqlInjection
scope :without_particular_labels, ->(label_names) do
labels_table = Label.arel_table
@@ -469,9 +468,11 @@ module Issuable
if self.respond_to?(:total_time_spent)
old_total_time_spent = old_associations.fetch(:total_time_spent, total_time_spent)
+ old_time_change = old_associations.fetch(:time_change, time_change)
if old_total_time_spent != total_time_spent
changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ changes[:time_change] = [old_time_change, time_change]
end
end
end
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 28d12a033a6..933e8b5f687 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -11,7 +11,8 @@ module IssueAvailableFeatures
def available_features_for_issue_types
{
assignee: %w(issue incident),
- confidentiality: %(issue incident)
+ confidentiality: %w(issue incident),
+ time_tracking: %w(issue incident)
}.with_indifferent_access
end
end
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index 672bcdbbb1b..41efea65c5a 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -6,6 +6,7 @@ module Limitable
included do
class_attribute :limit_scope
+ class_attribute :limit_relation
class_attribute :limit_name
class_attribute :limit_feature_flag
self.limit_name = self.name.demodulize.tableize
@@ -28,7 +29,7 @@ module Limitable
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)
+ relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
limits = scope_relation.actual_limits
check_plan_limit_not_exceeded(limits, relation)
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index e33b6db0103..b05beb6c764 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -29,7 +29,7 @@ module Mentionable
def self.external_pattern
strong_memoize(:external_pattern) do
- issue_pattern = IssueTrackerService.reference_pattern
+ issue_pattern = Integrations::BaseIssueTracker.reference_pattern
link_patterns = URI::DEFAULT_PARSER.make_regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern)
end
diff --git a/app/models/concerns/notification_branch_selection.rb b/app/models/concerns/notification_branch_selection.rb
index 2354335469a..18ec996c3df 100644
--- a/app/models/concerns/notification_branch_selection.rb
+++ b/app/models/concerns/notification_branch_selection.rb
@@ -2,7 +2,7 @@
# Concern handling functionality around deciding whether to send notification
# for activities on a specified branch or not. Will be included in
-# ChatNotificationService and PipelinesEmailService classes.
+# Integrations::BaseChatNotification and PipelinesEmailService classes.
module NotificationBranchSelection
extend ActiveSupport::Concern
diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb
index c41635a0d16..9cf66c756a0 100644
--- a/app/models/concerns/packages/debian/component_file.rb
+++ b/app/models/concerns/packages/debian/component_file.rb
@@ -50,6 +50,8 @@ module Packages
scope :with_file_type, ->(file_type) { where(file_type: file_type) }
+ scope :with_architecture, ->(architecture) { where(architecture: architecture) }
+
scope :with_architecture_name, ->(architecture_name) do
left_outer_joins(:architecture)
.where("packages_debian_#{container_type}_architectures" => { name: architecture_name })
@@ -60,7 +62,7 @@ module Packages
scope :preload_distribution, -> { includes(component: :distribution) }
- scope :created_before, ->(reference) { where("#{table_name}.created_at < ?", reference) }
+ scope :updated_before, ->(reference) { where("#{table_name}.updated_at < ?", reference) }
mount_file_store_uploader Packages::Debian::ComponentFileUploader
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 267c7a4d201..159f0044c82 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -18,6 +18,10 @@ module Packages
belongs_to container_type
belongs_to :creator, class_name: 'User'
+ has_one :key,
+ class_name: "Packages::Debian::#{container_type.capitalize}DistributionKey",
+ foreign_key: :distribution_id,
+ inverse_of: :distribution
# component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :components,
class_name: "Packages::Debian::#{container_type.capitalize}Component",
@@ -91,6 +95,14 @@ module Packages
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
+ def component_names
+ components.pluck(:name).sort
+ end
+
+ def architecture_names
+ architectures.pluck(:name).sort
+ end
+
def needs_update?
!file.exists? || time_duration_expired?
end
diff --git a/app/models/concerns/packages/debian/distribution_key.rb b/app/models/concerns/packages/debian/distribution_key.rb
new file mode 100644
index 00000000000..7023e2dcd37
--- /dev/null
+++ b/app/models/concerns/packages/debian/distribution_key.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ module DistributionKey
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :key
+ validates :distribution,
+ presence: true
+
+ validates :private_key, presence: true, length: { maximum: 512.kilobytes }
+ validates :passphrase, presence: true, length: { maximum: 255 }
+ validates :public_key, presence: true, length: { maximum: 512.kilobytes }
+ validates :fingerprint, presence: true, length: { maximum: 255 }
+
+ validate :private_key_armored, :public_key_armored
+
+ attr_encrypted :private_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm'
+ attr_encrypted :passphrase,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm'
+
+ private
+
+ def private_key_armored
+ if private_key.present? && !private_key.start_with?('-----BEGIN PGP PRIVATE KEY BLOCK-----')
+ errors.add(:private_key, 'must be ASCII armored')
+ end
+ end
+
+ def public_key_armored
+ if public_key.present? && !public_key.start_with?('-----BEGIN PGP PUBLIC KEY BLOCK-----')
+ errors.add(:public_key, 'must be ASCII armored')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index afebc426762..86280097d19 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -38,7 +38,7 @@ module PrometheusAdapter
# This is a heavy-weight check if a prometheus is properly configured and accessible from GitLab.
# This actually sends a request to an external service and often it could take a long time,
- # Please consider using `configured?` instead if the process is running on unicorn/puma threads.
+ # Please consider using `configured?` instead if the process is running on Puma threads.
def can_query?
prometheus_client.present?
end
diff --git a/app/models/concerns/service_push_data_validations.rb b/app/models/concerns/service_push_data_validations.rb
index defc5794142..451804a2c56 100644
--- a/app/models/concerns/service_push_data_validations.rb
+++ b/app/models/concerns/service_push_data_validations.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-# This concern is used by registerd services such as TeamCityService and
-# DroneCiService and add methods to perform validations on the received
+# This concern is used by registered integrations such as Integrations::TeamCity and
+# Integrations::DroneCi and adds methods to perform validations on the received
# data.
module ServicePushDataValidations
diff --git a/app/models/concerns/taggable_queries.rb b/app/models/concerns/taggable_queries.rb
new file mode 100644
index 00000000000..2897e5e6420
--- /dev/null
+++ b/app/models/concerns/taggable_queries.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module TaggableQueries
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # context is a name `acts_as_taggable context`
+ def arel_tag_names_array(context = :tags)
+ ActsAsTaggableOn::Tagging
+ .joins(:tag)
+ .where("taggings.taggable_id=#{quoted_table_name}.id") # rubocop:disable GitlabSecurity/SqlInjection
+ .where(taggings: { context: context, taggable_type: polymorphic_name })
+ .select('COALESCE(array_agg(tags.name ORDER BY name), ARRAY[]::text[])')
+ end
+ end
+end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index a1e7d06b1c1..89b42eec727 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -33,11 +33,11 @@ module TimeTrackable
return if @time_spent == 0
- if @time_spent == :reset
- reset_spent_time
- else
- add_or_subtract_spent_time
- end
+ @timelog = if @time_spent == :reset
+ reset_spent_time
+ else
+ add_or_subtract_spent_time
+ end
end
alias_method :spend_time=, :spend_time
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -50,6 +50,14 @@ module TimeTrackable
Gitlab::TimeTrackingFormatter.output(total_time_spent)
end
+ def time_change
+ @timelog&.time_spent.to_i # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def human_time_change
+ Gitlab::TimeTrackingFormatter.output(time_change)
+ end
+
def human_time_estimate
Gitlab::TimeTrackingFormatter.output(time_estimate)
end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index fb9a8cd312d..8dc58f8dca1 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -44,7 +44,6 @@ module Timebox
validates :project, presence: true, unless: :group
validates :title, presence: true
- validate :uniqueness_of_title, if: :title_changed?
validate :timebox_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits
@@ -243,18 +242,6 @@ module Timebox
end
end
- # Timebox titles must be unique across project and group timeboxes
- def uniqueness_of_title
- if project
- relation = self.class.for_projects_and_groups([project_id], [project.group&.id])
- elsif group
- relation = self.class.for_projects_and_groups(group.projects.select(:id), [group.id])
- end
-
- title_exists = relation.find_by_title(title)
- errors.add(:title, _("already being used for another group or project %{timebox_name}.") % { timebox_name: timebox_name }) if title_exists
- end
-
# Timebox should be either a project timebox or a group timebox
def timebox_type_check
if group_id && project_id
diff --git a/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb b/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
index 25c050820d6..3be82ed72d3 100644
--- a/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
+++ b/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
@@ -5,10 +5,6 @@ module TokenAuthenticatableStrategies
DYNAMIC_NONCE_IDENTIFIER = "|"
NONCE_SIZE = 12
- def self.encrypt_token(plaintext_token)
- Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token)
- end
-
def self.decrypt_token(token)
return unless token
@@ -22,5 +18,13 @@ module TokenAuthenticatableStrategies
Gitlab::CryptoHelper.aes256_gcm_decrypt(token)
end
end
+
+ def self.encrypt_token(plaintext_token)
+ return Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token) unless Feature.enabled?(:dynamic_nonce, type: :ops)
+
+ iv = ::Digest::SHA256.hexdigest(plaintext_token).bytes.take(NONCE_SIZE).pack('c*')
+ token = Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext_token, nonce: iv)
+ "#{DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv}"
+ end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 6e0d0e347c9..2d28a81f462 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -24,8 +24,15 @@ class ContainerRepository < ApplicationRecord
scope :for_group_and_its_subgroups, ->(group) do
project_scope = Project
.for_group_and_its_subgroups(group)
- .with_container_registry
- .select(:id)
+
+ project_scope =
+ if Feature.enabled?(:read_container_registry_access_level, group, default_enabled: :yaml)
+ project_scope.with_feature_enabled(:container_registry)
+ else
+ project_scope.with_container_registry
+ end
+
+ project_scope = project_scope.select(:id)
joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id")
end
@@ -33,6 +40,7 @@ class ContainerRepository < ApplicationRecord
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) }
+ scope :with_stale_ongoing_cleanup, ->(threshold) { cleanup_ongoing.where('expiration_policy_started_at < ?', threshold) }
def self.exists_by_path?(path)
where(
@@ -42,16 +50,15 @@ class ContainerRepository < ApplicationRecord
end
def self.with_enabled_policy
- joins("INNER JOIN container_expiration_policies ON container_repositories.project_id = container_expiration_policies.project_id")
+ 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)
- )
+ with_enabled_policy
+ .where(container_repositories: { expiration_policy_cleanup_status: REQUIRING_CLEANUP_STATUSES })
+ .where('container_repositories.expiration_policy_started_at IS NULL OR container_repositories.expiration_policy_started_at < container_expiration_policies.next_run_at')
+ .where('container_expiration_policies.next_run_at < ?', Time.zone.now)
end
def self.with_unfinished_cleanup
diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb
deleted file mode 100644
index 5bd07b3f6c3..00000000000
--- a/app/models/cycle_analytics/project_level.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module CycleAnalytics
- class ProjectLevel
- attr_reader :project, :options
-
- def initialize(project, options:)
- @project = project
- @options = options.merge(project: project)
- end
-
- def summary
- @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(project,
- from: options[:from],
- to: options[:to],
- current_user: options[:current_user]).data
- end
-
- def permissions(user:)
- Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
- end
-
- def stats
- @stats ||= default_stage_names.map do |stage_name|
- self[stage_name].as_json
- end
- end
-
- def [](stage_name)
- CycleAnalytics::ProjectLevelStageAdapter.new(build_stage(stage_name), options)
- end
-
- private
-
- def build_stage(stage_name)
- stage_params = stage_params_by_name(stage_name).merge(project: project)
- Analytics::CycleAnalytics::ProjectStage.new(stage_params)
- end
-
- def stage_params_by_name(name)
- Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name)
- end
-
- def default_stage_names
- Gitlab::Analytics::CycleAnalytics::DefaultStages.symbolized_stage_names
- end
- end
-end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index e2b25690323..7f5849bffc6 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -8,6 +8,9 @@ class Deployment < ApplicationRecord
include Importable
include Gitlab::Utils::StrongMemoize
include FastDestroyAll
+ include IgnorableColumns
+
+ ignore_column :deployable_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
belongs_to :project, required: true
belongs_to :environment, required: true
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 2e677a3d177..558963c98c4 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -223,6 +223,7 @@ class Environment < ApplicationRecord
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
.append(key: 'CI_ENVIRONMENT_SLUG', value: slug)
+ .append(key: 'CI_ENVIRONMENT_TIER', value: tier)
end
def recently_updated_on_branch?(ref)
@@ -335,10 +336,6 @@ class Environment < ApplicationRecord
prometheus_adapter.query(:environment, self) if has_metrics_and_can_query?
end
- def prometheus_status
- deployment_platform&.cluster&.application_prometheus&.status_name
- end
-
def additional_metrics(*args)
return unless has_metrics_and_can_query?
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 55ea4e2fe18..07c0983f239 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -100,7 +100,7 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments.includes(:project).available.map do |environment|
+ pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
index 7ffb321f2b7..cd0814c476a 100644
--- a/app/models/experiment.rb
+++ b/app/models/experiment.rb
@@ -11,7 +11,11 @@ class Experiment < ApplicationRecord
end
def self.add_group(name, variant:, group:)
- find_or_create_by!(name: name).record_group_and_variant!(group, variant)
+ add_subject(name, variant: variant, subject: group)
+ end
+
+ def self.add_subject(name, variant:, subject:)
+ find_or_create_by!(name: name).record_subject_and_variant!(subject, variant)
end
def self.record_conversion_event(name, user, context = {})
@@ -37,8 +41,11 @@ class Experiment < ApplicationRecord
experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context))
end
- def record_group_and_variant!(group, variant)
- experiment_subject = experiment_subjects.find_or_initialize_by(group: group)
+ def record_subject_and_variant!(subject, variant)
+ raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
+
+ attr_name = subject.class.table_name.singularize.to_sym
+ experiment_subject = experiment_subjects.find_or_initialize_by(attr_name => subject)
experiment_subject.assign_attributes(variant: variant)
# We only call save when necessary because this causes the request to stick to the primary DB
# even when the save is a no-op
diff --git a/app/models/experiment_subject.rb b/app/models/experiment_subject.rb
index 51ffc0b304e..2a7b9017a51 100644
--- a/app/models/experiment_subject.rb
+++ b/app/models/experiment_subject.rb
@@ -5,7 +5,7 @@ class ExperimentSubject < ApplicationRecord
belongs_to :experiment, inverse_of: :experiment_subjects
belongs_to :user
- belongs_to :group
+ belongs_to :namespace
belongs_to :project
validates :experiment, presence: true
@@ -14,15 +14,19 @@ class ExperimentSubject < ApplicationRecord
enum variant: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 }
+ def self.valid_subject?(subject)
+ subject.is_a?(Namespace) || subject.is_a?(User) || subject.is_a?(Project)
+ end
+
private
def must_have_one_subject_present
if non_nil_subjects.length != 1
- errors.add(:base, s_("ExperimentSubject|Must have exactly one of User, Group, or Project."))
+ errors.add(:base, s_("ExperimentSubject|Must have exactly one of User, Namespace, or Project."))
end
end
def non_nil_subjects
- @non_nil_subjects ||= [user, group, project].reject(&:blank?)
+ @non_nil_subjects ||= [user, namespace, project].reject(&:blank?)
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index da795651c63..e4127b2b2d4 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -16,9 +16,7 @@ class Group < Namespace
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
include EachBatch
- include HasTimelogsReport
-
- ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
+ include BulkMemberAccessLoad
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
@@ -82,6 +80,8 @@ class Group < Namespace
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ delegate :prevent_sharing_groups_outside_hierarchy, to: :namespace_settings
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
@@ -444,6 +444,12 @@ class Group < Namespace
end
end
+ def self_and_descendants_ids
+ strong_memoize(:self_and_descendants_ids) do
+ self_and_descendants.pluck(:id)
+ end
+ end
+
def direct_members
GroupMember.active_without_invites_and_requests
.non_minimal_access
@@ -569,24 +575,8 @@ 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
-
- 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
+ max_member_access([user.id])[user.id]
end
def mattermost_team_params
@@ -649,7 +639,7 @@ class Group < Namespace
end
def access_request_approvers_to_be_notified
- members.owners.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ members.owners.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def supports_events?
@@ -657,13 +647,17 @@ class Group < Namespace
end
def export_file_exists?
- export_file&.file
+ import_export_upload&.export_file_exists?
end
def export_file
import_export_upload&.export_file
end
+ def export_archive_exists?
+ import_export_upload&.export_archive_exists?
+ end
+
def adjourned_deletion?
false
end
@@ -728,8 +722,26 @@ class Group < Namespace
Gitlab::Routing.url_helpers.activity_group_path(self)
end
+ # rubocop: disable CodeReuse/ServiceClass
+ def open_issues_count(current_user = nil)
+ Groups::OpenIssuesCountService.new(self, current_user).count
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
+ # rubocop: disable CodeReuse/ServiceClass
+ def open_merge_requests_count(current_user = nil)
+ Groups::MergeRequestsCountService.new(self, current_user).count
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
private
+ def max_member_access(user_ids)
+ max_member_access_for_resource_ids(User, user_ids) do |user_ids|
+ members_with_parents.where(user_id: user_ids).group(:user_id).maximum(:access_level)
+ end
+ end
+
def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb
index d4ad29ddabb..084a8672460 100644
--- a/app/models/group_deploy_token.rb
+++ b/app/models/group_deploy_token.rb
@@ -9,8 +9,6 @@ class GroupDeployToken < ApplicationRecord
validates :deploy_token_id, uniqueness: { scope: [:group_id] }
def has_access_to?(requested_project)
- return false unless Feature.enabled?(:allow_group_deploy_token, default_enabled: true)
-
requested_project_group = requested_project&.group
return false unless requested_project_group
return true if requested_project_group.id == group_id
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index a28b97e63e5..d1584a62bfb 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -39,6 +39,11 @@ class ProjectHook < WebHook
def rate_limit
project.actual_limits.limit_for(:web_hook_calls)
end
+
+ override :application_context
+ def application_context
+ super.merge(project: project)
+ end
end
ProjectHook.prepend_mod_with('ProjectHook')
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 02b4feb4ccc..5f8fa4bca0a 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,6 +3,7 @@
class WebHook < ApplicationRecord
include Sortable
+ MAX_FAILURES = 100
FAILURE_THRESHOLD = 3 # three strikes
INITIAL_BACKOFF = 10.minutes
MAX_BACKOFF = 1.day
@@ -72,14 +73,29 @@ class WebHook < ApplicationRecord
end
def enable!
+ return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
+
update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
end
+ def backoff!
+ update!(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
+ end
+
+ def failed!
+ update!(recent_failures: recent_failures + 1) if recent_failures < MAX_FAILURES
+ end
+
# Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
def rate_limit
nil
end
+ # Custom attributes to be included in the worker context.
+ def application_context
+ { related_class: type }
+ end
+
private
def web_hooks_disable_failed?
diff --git a/app/models/hooks/web_hook_log_archived.rb b/app/models/hooks/web_hook_log_archived.rb
deleted file mode 100644
index a1c8a44f5ba..00000000000
--- a/app/models/hooks/web_hook_log_archived.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# 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/import_export_upload.rb b/app/models/import_export_upload.rb
index 7d73fd281f1..bc363cce8dd 100644
--- a/app/models/import_export_upload.rb
+++ b/app/models/import_export_upload.rb
@@ -11,7 +11,42 @@ class ImportExportUpload < ApplicationRecord
mount_uploader :import_file, ImportExportUploader
mount_uploader :export_file, ImportExportUploader
+ # This causes CarrierWave v1 and v3 (but not v2) to upload the file to
+ # object storage *after* the database entry has been committed to the
+ # database. This avoids idling in a transaction.
+ if Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_STORE_EXPORT_FILE_AFTER_COMMIT', true))
+ skip_callback :save, :after, :store_export_file!
+ set_callback :commit, :after, :store_export_file!
+ end
+
+ scope :updated_before, ->(date) { where('updated_at < ?', date) }
+ scope :with_export_file, -> { where.not(export_file: nil) }
+
def retrieve_upload(_identifier, paths)
Upload.find_by(model: self, path: paths)
end
+
+ def export_file_exists?
+ !!carrierwave_export_file
+ end
+
+ # This checks if the export archive is actually stored on disk. It
+ # requires a HEAD request if object storage is used.
+ def export_archive_exists?
+ !!carrierwave_export_file&.exists?
+ # Handle any HTTP unexpected error
+ # https://github.com/excon/excon/blob/bbb5bd791d0bb2251593b80e3bce98dbec6e8f24/lib/excon/error.rb#L129-L169
+ rescue Excon::Error => e
+ # The HEAD request will fail with a 403 Forbidden if the file does not
+ # exist, and the user does not have permission to list the object
+ # storage bucket.
+ Gitlab::ErrorTracking.track_exception(e)
+ false
+ end
+
+ private
+
+ def carrierwave_export_file
+ export_file&.file
+ end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 13203cd4e95..238ecbbf209 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -6,7 +6,7 @@ class Integration < ApplicationRecord
include Sortable
include Importable
include ProjectServicesLoggable
- include DataFields
+ include Integrations::HasDataFields
include FromUnion
include EachBatch
@@ -29,6 +29,27 @@ class Integration < ApplicationRecord
mock_ci mock_monitoring
].freeze
+ # Base classes which aren't actual integrations.
+ BASE_CLASSES = %w[
+ Integrations::BaseChatNotification
+ Integrations::BaseCi
+ Integrations::BaseIssueTracker
+ Integrations::BaseMonitoring
+ Integrations::BaseSlashCommands
+ ].freeze
+
+ # used as part of the renaming effort (https://gitlab.com/groups/gitlab-org/-/epics/2504)
+ RENAMED_TO_INTEGRATION = %w[
+ asana assembla
+ bamboo bugzilla buildkite
+ campfire confluence custom_issue_tracker
+ datadog discord drone_ci
+ ].to_set.freeze
+
+ def self.renamed?(name)
+ RENAMED_TO_INTEGRATION.include?(name)
+ end
+
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
attribute :type, Gitlab::Integrations::StiType.new
@@ -59,7 +80,7 @@ class Integration < ApplicationRecord
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, presence: true, exclusion: BASE_CLASSES
validates :type, uniqueness: { scope: :template }, if: :template?
validates :type, uniqueness: { scope: :instance }, if: :instance_level?
validates :type, uniqueness: { scope: :project_id }, if: :project_level?
@@ -185,7 +206,7 @@ class Integration < ApplicationRecord
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
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)
+ integration_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)
@@ -194,7 +215,7 @@ class Integration < ApplicationRecord
def self.build_nonexistent_services_for(scope)
nonexistent_services_types_for(scope).map do |service_type|
- service_type_to_model(service_type).new
+ integration_type_to_model(service_type).new
end
end
private_class_method :build_nonexistent_services_for
@@ -210,6 +231,7 @@ class Integration < ApplicationRecord
# Returns a list of available service names.
# Example: ["asana", ...]
+ # @deprecated
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
@@ -218,10 +240,14 @@ class Integration < ApplicationRecord
service_names.sort_by(&:downcase)
end
- def self.services_names
+ def self.integration_names
INTEGRATION_NAMES
end
+ def self.services_names
+ integration_names
+ end
+
def self.dev_services_names
return [] unless Rails.env.development?
@@ -236,29 +262,29 @@ class Integration < ApplicationRecord
# 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_to_type(service_name)
+ integration_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)
+ def self.integration_name_to_model(name)
+ type = integration_name_to_type(name)
+ integration_type_to_model(type)
end
# Returns the STI type for the given service name.
# Example: "asana" => "AsanaService"
- def self.service_name_to_type(name)
+ def self.integration_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)
+ def self.integration_type_to_model(type)
Gitlab::Integrations::StiType.new.cast(type).constantize
end
- private_class_method :service_type_to_model
+ private_class_method :integration_type_to_model
def self.build_from_integration(integration, project_id: nil, group_id: nil)
new_integration = integration.dup
@@ -480,10 +506,6 @@ class Integration < ApplicationRecord
ProjectServiceWorker.perform_async(id, data)
end
- def external_wiki?
- type == 'ExternalWikiService' && active?
- end
-
# override if needed
def supports_data_fields?
false
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 82111c7322e..dbd7aedf4fe 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Integrations
- class Bamboo < CiService
+ class Bamboo < BaseCi
include ActionView::Helpers::UrlHelper
include ReactiveService
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
new file mode 100644
index 00000000000..5eae8bce92a
--- /dev/null
+++ b/app/models/integrations/base_chat_notification.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+# Base class for Chat notifications services
+# This class is not meant to be used directly, but only to inherit from.
+
+module Integrations
+ class BaseChatNotification < Integration
+ include ChatMessage
+ include NotificationBranchSelection
+
+ SUPPORTED_EVENTS = %w[
+ push issue confidential_issue merge_request note confidential_note
+ tag_push pipeline wiki_page deployment
+ ].freeze
+
+ SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
+
+ 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, :labels_to_be_notified_behavior
+
+ # Custom serialized properties initialization
+ prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
+
+ 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
+ # `branches_to_be_notified`. Instead of doing a background migration, we
+ # opted to set a value for the new property based on the old one, if
+ # users haven't specified one already. When users edit the service and
+ # select a value for this new property, it will override everything.
+
+ self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
+ end
+ end
+
+ def confidential_issue_channel
+ properties['confidential_issue_channel'].presence || properties['issue_channel']
+ end
+
+ def confidential_note_channel
+ properties['confidential_note_channel'].presence || properties['note_channel']
+ end
+
+ def self.supported_events
+ SUPPORTED_EVENTS
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
+ { 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: '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
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ return unless webhook.present?
+
+ object_kind = data[:object_kind]
+
+ data = custom_data(data)
+
+ return unless notify_label?(data)
+
+ # WebHook events often have an 'update' event that follows a 'open' or
+ # 'close' action. Ignore update events for now to prevent duplicate
+ # messages from arriving.
+
+ message = get_message(object_kind, data)
+
+ return false unless message
+
+ event_type = data[:event_type] || object_kind
+
+ channel_names = get_channel_field(event_type).presence || channel.presence
+ channels = channel_names&.split(',')&.map(&:strip)
+
+ opts = {}
+ opts[:channel] = channels if channels.present?
+ opts[:username] = username if username
+
+ if notify(message, opts)
+ log_usage(event_type, user_id_from_hook_data(data))
+ return true
+ end
+
+ false
+ end
+
+ def event_channel_names
+ supported_events.map { |event| event_channel_name(event) }
+ end
+
+ def event_field(event)
+ fields.find { |field| field[:name] == event_channel_name(event) }
+ end
+
+ def global_fields
+ fields.reject { |field| field[:name].end_with?('channel') }
+ end
+
+ def default_channel_placeholder
+ raise NotImplementedError
+ end
+
+ private
+
+ def log_usage(_, _)
+ # Implement in child class
+ end
+
+ def labels_to_be_notified_list
+ return [] if labels_to_be_notified.nil?
+
+ labels_to_be_notified.delete('~').split(',').map(&:strip)
+ end
+
+ def notify_label?(data)
+ return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
+
+ labels = data[:labels] || data.dig(:issue, :labels) || data.dig(:merge_request, :labels) || data.dig(:object_attributes, :labels)
+
+ return false if labels.blank?
+
+ 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)
+ data.dig(:user, :id) || data[:user_id]
+ end
+
+ # every notifier must implement this independently
+ def notify(message, opts)
+ raise NotImplementedError
+ end
+
+ def custom_data(data)
+ data.merge(project_url: project_url, project_name: project_name).with_indifferent_access
+ end
+
+ def get_message(object_kind, data)
+ case object_kind
+ when "push", "tag_push"
+ Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
+ when "issue"
+ Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
+ when "merge_request"
+ Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
+ when "note"
+ Integrations::ChatMessage::NoteMessage.new(data)
+ when "pipeline"
+ Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ when "wiki_page"
+ Integrations::ChatMessage::WikiPageMessage.new(data)
+ when "deployment"
+ Integrations::ChatMessage::DeploymentMessage.new(data)
+ end
+ end
+
+ def get_channel_field(event)
+ field_name = event_channel_name(event)
+ self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def build_event_channels
+ supported_events.reduce([]) do |channels, event|
+ channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
+ end
+ end
+
+ def event_channel_name(event)
+ EVENT_CHANNEL[event]
+ end
+
+ def project_name
+ project.full_name
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def update?(data)
+ data[:object_attributes][:action] == 'update'
+ end
+
+ def should_pipeline_be_notified?(data)
+ notify_for_ref?(data) && notify_for_pipeline?(data)
+ end
+
+ def notify_for_ref?(data)
+ return true if data[:object_kind] == 'tag_push'
+ return true if data.dig(:object_attributes, :tag)
+
+ notify_for_branch?(data)
+ end
+
+ def notify_for_pipeline?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/base_ci.rb b/app/models/integrations/base_ci.rb
new file mode 100644
index 00000000000..b2e269b1b50
--- /dev/null
+++ b/app/models/integrations/base_ci.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# Base class for CI services
+# List methods you need to implement to get your CI service
+# working with GitLab merge requests
+module Integrations
+ class BaseCi < Integration
+ default_value_for :category, 'ci'
+
+ def valid_token?(token)
+ self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ # Return complete url to build page
+ #
+ # Ex.
+ # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
+ #
+ def build_page(sha, ref)
+ # implement inside child
+ end
+
+ # Return string with build status or :error symbol
+ #
+ # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
+ #
+ #
+ # Ex.
+ # @service.commit_status('13be4ac', 'master')
+ # # => 'success'
+ #
+ # @service.commit_status('2abe4ac', 'dev')
+ # # => 'running'
+ #
+ #
+ def commit_status(sha, ref)
+ # implement inside child
+ end
+ end
+end
diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb
new file mode 100644
index 00000000000..6c24f762cd5
--- /dev/null
+++ b/app/models/integrations/base_issue_tracker.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+module Integrations
+ class BaseIssueTracker < Integration
+ validate :one_issue_tracker, if: :activated?, on: :manual_change
+
+ # TODO: we can probably just delegate as part of
+ # https://gitlab.com/gitlab-org/gitlab/issues/29404
+ data_field :project_url, :issues_url, :new_issue_url
+
+ default_value_for :category, 'issue_tracker'
+
+ before_validation :handle_properties
+ before_validation :set_default_data, on: :create
+
+ # Pattern used to extract links from comments
+ # Override this method on services that uses different patterns
+ # This pattern does not support cross-project references
+ # The other code assumes that this pattern is a superset of all
+ # overridden patterns. See ReferenceRegexes.external_pattern
+ def self.reference_pattern(only_long: false)
+ if only_long
+ /(\b[A-Z][A-Z0-9_]*-)#{Gitlab::Regex.issue}/
+ else
+ /(\b[A-Z][A-Z0-9_]*-|#{Issue.reference_prefix})#{Gitlab::Regex.issue}/
+ end
+ end
+
+ def handle_properties
+ # this has been moved from initialize_properties and should be improved
+ # as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
+ return unless properties
+
+ @legacy_properties_data = properties.dup
+ data_values = properties.slice!('title', 'description')
+ data_values.reject! { |key| data_fields.changed.include?(key) }
+ data_values.slice!(*data_fields.attributes.keys)
+ data_fields.assign_attributes(data_values) if data_values.present?
+
+ self.properties = {}
+ end
+
+ def legacy_properties_data
+ @legacy_properties_data ||= {}
+ end
+
+ def supports_data_fields?
+ true
+ end
+
+ def data_fields
+ issue_tracker_data || self.build_issue_tracker_data
+ end
+
+ def default?
+ default
+ end
+
+ def issue_url(iid)
+ issues_url.gsub(':id', iid.to_s)
+ end
+
+ def issue_tracker_path
+ project_url
+ end
+
+ def new_issue_path
+ new_issue_url
+ end
+
+ def issue_path(iid)
+ issue_url(iid)
+ end
+
+ def fields
+ [
+ { 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
+
+ def initialize_properties
+ {}
+ end
+
+ # Initialize with default properties values
+ def set_default_data
+ return unless issues_tracker.present?
+
+ # we don't want to override if we have set something
+ return if project_url || issues_url || new_issue_url
+
+ data_fields.project_url = issues_tracker['project_url']
+ data_fields.issues_url = issues_tracker['issues_url']
+ data_fields.new_issue_url = issues_tracker['new_issue_url']
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again."
+ result = false
+
+ begin
+ response = Gitlab::HTTP.head(self.project_url, verify: true)
+
+ if response
+ message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
+ result = true
+ end
+ rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
+ message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
+ end
+ log_info(message)
+ result
+ end
+
+ def support_close_issue?
+ false
+ end
+
+ def support_cross_reference?
+ false
+ end
+
+ def create_cross_reference_note(mentioned, noteable, author)
+ # implement inside child
+ end
+
+ private
+
+ def enabled_in_gitlab_config
+ Gitlab.config.issues_tracker &&
+ Gitlab.config.issues_tracker.values.any? &&
+ issues_tracker
+ end
+
+ def issues_tracker
+ Gitlab.config.issues_tracker[to_param]
+ end
+
+ def one_issue_tracker
+ return if template? || instance?
+ return if project.blank?
+
+ 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
+end
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
new file mode 100644
index 00000000000..eacf1184aae
--- /dev/null
+++ b/app/models/integrations/base_slash_commands.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# Base class for ChatOps integrations
+# This class is not meant to be used directly, but only to inherrit from.
+module Integrations
+ class BaseSlashCommands < Integration
+ default_value_for :category, 'chat'
+
+ prop_accessor :token
+
+ has_many :chat_names, foreign_key: :service_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ def valid_token?(token)
+ self.respond_to?(:token) &&
+ self.token.present? &&
+ ActiveSupport::SecurityUtils.secure_compare(token, self.token)
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def can_test?
+ false
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
+ ]
+ end
+
+ def trigger(params)
+ return unless valid_token?(params[:token])
+
+ chat_user = find_chat_user(params)
+ user = chat_user&.user
+
+ if user
+ unless user.can?(:use_slash_commands)
+ return Gitlab::SlashCommands::Presenters::Access.new.deactivated if user.deactivated?
+
+ return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project)
+ end
+
+ Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
+ else
+ url = authorize_chat_name_url(params)
+ Gitlab::SlashCommands::Presenters::Access.new(url).authorize
+ end
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ServiceClass
+ def find_chat_user(params)
+ ChatNames::FindUserService.new(self, params).execute
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
+ # rubocop: disable CodeReuse/ServiceClass
+ def authorize_chat_name_url(params)
+ ChatNames::AuthorizeUserService.new(self, params).execute
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+ end
+end
diff --git a/app/models/integrations/bugzilla.rb b/app/models/integrations/bugzilla.rb
new file mode 100644
index 00000000000..9251015acb8
--- /dev/null
+++ b/app/models/integrations/bugzilla.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Bugzilla < BaseIssueTracker
+ include ActionView::Helpers::UrlHelper
+
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
+
+ def title
+ 'Bugzilla'
+ end
+
+ def description
+ 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
+ 'bugzilla'
+ end
+ end
+end
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
new file mode 100644
index 00000000000..906a5d02f9c
--- /dev/null
+++ b/app/models/integrations/buildkite.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require "addressable/uri"
+
+module Integrations
+ class Buildkite < BaseCi
+ include ReactiveService
+
+ ENDPOINT = "https://buildkite.com"
+
+ prop_accessor :project_url, :token
+
+ validates :project_url, presence: true, public_url: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+
+ after_save :compose_service_hook, if: :activated?
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ # This is a stub method to work with deprecated API response
+ # TODO: remove enable_ssl_verification after 14.0
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/222808
+ def enable_ssl_verification
+ true
+ end
+
+ # Since SSL verification will always be enabled for Buildkite,
+ # we no longer needs to store the boolean.
+ # This is a stub method to work with deprecated API param.
+ # TODO: remove enable_ssl_verification after 14.0
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/222808
+ def enable_ssl_verification=(_value)
+ self.properties.delete('enable_ssl_verification') # Remove unused key
+ end
+
+ def webhook_url
+ "#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = webhook_url
+ hook.enable_ssl_verification = true
+ hook.save
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ service_hook.execute(data)
+ end
+
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
+
+ def commit_status_path(sha)
+ "#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
+ end
+
+ def build_page(sha, ref)
+ "#{project_url}/builds?commit=#{sha}"
+ end
+
+ def title
+ 'Buildkite'
+ end
+
+ def description
+ 'Run CI/CD pipelines with Buildkite.'
+ end
+
+ def self.to_param
+ 'buildkite'
+ end
+
+ def fields
+ [
+ { type: 'text',
+ name: 'token',
+ title: 'Integration Token',
+ help: 'This token will be provided when you create a Buildkite pipeline with a GitLab repository',
+ required: true },
+
+ { type: 'text',
+ name: 'project_url',
+ title: 'Pipeline URL',
+ placeholder: "#{ENDPOINT}/acme-inc/test-pipeline",
+ required: true }
+ ]
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
+
+ status =
+ if response&.code == 200 && response['status']
+ response['status']
+ else
+ :error
+ end
+
+ { commit_status: status }
+ end
+
+ private
+
+ def webhook_token
+ token_parts.first
+ end
+
+ def status_token
+ token_parts.second
+ end
+
+ def token_parts
+ if token.present?
+ token.split(':')
+ else
+ []
+ end
+ end
+
+ def buildkite_endpoint(subdomain = nil)
+ if subdomain.present?
+ uri = Addressable::URI.parse(ENDPOINT)
+ new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}"
+
+ if uri.port.present?
+ "#{new_endpoint}:#{uri.port}"
+ else
+ new_endpoint
+ end
+ else
+ ENDPOINT
+ end
+ end
+
+ def request_options
+ { verify: false, extra_log_info: { project_id: project_id } }
+ end
+ end
+end
diff --git a/app/models/integrations/builds_email.rb b/app/models/integrations/builds_email.rb
deleted file mode 100644
index 2628848667e..00000000000
--- a/app/models/integrations/builds_email.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# 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/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb
index 2f70384d3b9..afe3ffc45a0 100644
--- a/app/models/integrations/chat_message/base_message.rb
+++ b/app/models/integrations/chat_message/base_message.rb
@@ -58,7 +58,7 @@ module Integrations
end
def format(string)
- Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
+ ::Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
end
def format_relative_links(string)
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
index a0f6f582e4c..a3f68d34035 100644
--- a/app/models/integrations/chat_message/pipeline_message.rb
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -105,7 +105,7 @@ module Integrations
def failed_stages_field
{
title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
+ value: ::Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
short: true
}
end
@@ -113,7 +113,7 @@ module Integrations
def failed_jobs_field
{
title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
+ value: ::Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
short: true
}
end
@@ -130,12 +130,12 @@ module Integrations
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
- value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
+ value: ::Slack::Messenger::Util::LinkFormatter.format(ref_link),
short: true
},
{
title: s_("ChatMessage|Commit"),
- value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
+ value: ::Slack::Messenger::Util::LinkFormatter.format(commit_link),
short: true
}
]
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
index 0952986e923..fabd214633b 100644
--- a/app/models/integrations/chat_message/push_message.rb
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -49,7 +49,7 @@ module Integrations
end
def format(string)
- Slack::Messenger::Util::LinkFormatter.format(string)
+ ::Slack::Messenger::Util::LinkFormatter.format(string)
end
def commit_messages
diff --git a/app/models/integrations/chat_message/wiki_page_message.rb b/app/models/integrations/chat_message/wiki_page_message.rb
index 9b5275b8c03..00f0f911b0e 100644
--- a/app/models/integrations/chat_message/wiki_page_message.rb
+++ b/app/models/integrations/chat_message/wiki_page_message.rb
@@ -7,6 +7,7 @@ module Integrations
attr_reader :wiki_page_url
attr_reader :action
attr_reader :description
+ attr_reader :diff_url
def initialize(params)
super
@@ -16,6 +17,7 @@ module Integrations
@title = obj_attr[:title]
@wiki_page_url = obj_attr[:url]
@description = obj_attr[:message]
+ @diff_url = obj_attr[:diff_url]
@action =
case obj_attr[:action]
@@ -44,19 +46,23 @@ module Integrations
private
def message
- "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ "#{user_combined_name} #{action} #{wiki_page_link} (#{diff_link}) in #{project_link}: *#{title}*"
end
def description_message
[{ text: format(@description), color: attachment_color }]
end
+ def diff_link
+ link('Compare changes', diff_url)
+ end
+
def project_link
- "[#{project_name}](#{project_url})"
+ link(project_name, project_url)
end
def wiki_page_link
- "[wiki page](#{wiki_page_url})"
+ link('wiki page', wiki_page_url)
end
end
end
diff --git a/app/models/integrations/custom_issue_tracker.rb b/app/models/integrations/custom_issue_tracker.rb
new file mode 100644
index 00000000000..635a9d093e9
--- /dev/null
+++ b/app/models/integrations/custom_issue_tracker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Integrations
+ class CustomIssueTracker < BaseIssueTracker
+ include ActionView::Helpers::UrlHelper
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
+
+ def title
+ s_('IssueTracker|Custom issue tracker')
+ end
+
+ def description
+ s_("IssueTracker|Use a custom issue tracker 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/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 self.to_param
+ 'custom_issue_tracker'
+ end
+ end
+end
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
new file mode 100644
index 00000000000..ef6d46fd3d3
--- /dev/null
+++ b/app/models/integrations/discord.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require "discordrb/webhooks"
+
+module Integrations
+ class Discord < BaseChatNotification
+ include ActionView::Helpers::UrlHelper
+
+ ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
+
+ def title
+ s_("DiscordService|Discord Notifications")
+ end
+
+ def description
+ s_("DiscordService|Send notifications about project events to a Discord channel.")
+ end
+
+ def self.to_param
+ "discord"
+ end
+
+ def help
+ docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def event_field(event)
+ # No-op.
+ end
+
+ def default_channel_placeholder
+ # No-op.
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
+ { type: "checkbox", name: "notify_only_broken_pipelines" },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ client = Discordrb::Webhooks::Client.new(url: webhook)
+
+ client.execute do |builder|
+ builder.add_embed do |embed|
+ embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar)
+ embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n")
+ end
+ end
+ rescue RestClient::Exception => error
+ log_error(error.message)
+ false
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+ end
+end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
new file mode 100644
index 00000000000..096f7093b8c
--- /dev/null
+++ b/app/models/integrations/drone_ci.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Integrations
+ class DroneCi < BaseCi
+ include ReactiveService
+ include ServicePushDataValidations
+
+ prop_accessor :drone_url, :token
+ boolean_accessor :enable_ssl_verification
+
+ validates :drone_url, presence: true, public_url: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+
+ after_save :compose_service_hook, if: :activated?
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ # If using a service template, project may not be available
+ hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
+ hook.enable_ssl_verification = !!enable_ssl_verification
+ hook.save
+ end
+
+ def execute(data)
+ case data[:object_kind]
+ when 'push'
+ service_hook.execute(data) if push_valid?(data)
+ when 'merge_request'
+ service_hook.execute(data) if merge_request_valid?(data)
+ when 'tag_push'
+ service_hook.execute(data) if tag_push_valid?(data)
+ end
+ end
+
+ def allow_target_ci?
+ true
+ end
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def commit_status_path(sha, ref)
+ Gitlab::Utils.append_path(
+ drone_url,
+ "gitlab/#{project.full_path}/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}&access_token=#{token}")
+ end
+
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) { |cached| cached[:commit_status] }
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
+ verify: enable_ssl_verification,
+ extra_log_info: { project_id: project_id })
+
+ status =
+ if response && response.code == 200 && response['status']
+ case response['status']
+ when 'killed'
+ :canceled
+ when 'failure', 'error'
+ # Because drone return error if some test env failed
+ :failed
+ else
+ response["status"]
+ end
+ else
+ :error
+ end
+
+ { commit_status: status }
+ end
+
+ def build_page(sha, ref)
+ Gitlab::Utils.append_path(
+ drone_url,
+ "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
+ end
+
+ def title
+ 'Drone'
+ end
+
+ def description
+ s_('ProjectService|Run CI/CD pipelines with Drone.')
+ end
+
+ def self.to_param
+ 'drone_ci'
+ end
+
+ def help
+ s_('ProjectService|Run CI/CD pipelines with Drone.')
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', help: s_('ProjectService|Token for the Drone project.'), required: true },
+ { type: 'text', name: 'drone_url', title: s_('ProjectService|Drone server URL'), placeholder: 'http://drone.example.com', required: true },
+ { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
+ ]
+ end
+ end
+end
diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb
new file mode 100644
index 00000000000..0a4e8d92ed7
--- /dev/null
+++ b/app/models/integrations/ewm.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Ewm < BaseIssueTracker
+ 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)
+ @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
+ end
+
+ def title
+ 'EWM'
+ end
+
+ def description
+ 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
+ 'ewm'
+ end
+
+ def can_test?
+ false
+ end
+
+ def issue_url(iid)
+ issues_url.gsub(':id', iid.to_s.split(' ')[-1])
+ end
+ end
+end
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
new file mode 100644
index 00000000000..fec435443fa
--- /dev/null
+++ b/app/models/integrations/external_wiki.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ExternalWiki < Integration
+ include ActionView::Helpers::UrlHelper
+
+ prop_accessor :external_wiki_url
+ validates :external_wiki_url, presence: true, public_url: true, if: :activated?
+
+ def title
+ s_('ExternalWikiService|External wiki')
+ end
+
+ def description
+ s_('ExternalWikiService|Link to an external wiki from the sidebar.')
+ end
+
+ def self.to_param
+ 'external_wiki'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'external_wiki_url',
+ title: s_('ExternalWikiService|External wiki URL'),
+ placeholder: s_('ExternalWikiService|https://example.com/xxx/wiki/...'),
+ help: 'Enter the URL to the external wiki.',
+ required: true
+ }
+ ]
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer'
+
+ s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def execute(_data)
+ response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
+ response.body if response.code == 200
+ rescue StandardError
+ nil
+ end
+
+ def self.supported_events
+ %w()
+ end
+ end
+end
diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb
new file mode 100644
index 00000000000..443f61e65dd
--- /dev/null
+++ b/app/models/integrations/flowdock.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Flowdock < Integration
+ include ActionView::Helpers::UrlHelper
+
+ prop_accessor :token
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'Flowdock'
+ end
+
+ def description
+ 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
+ 'flowdock'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: s_('FlowdockService|1b609b52537...'), required: true, help: 'Enter your Flowdock token.' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ ::Flowdock::Git.post(
+ data[:ref],
+ data[:before],
+ data[:after],
+ token: token,
+ repo: project.repository,
+ repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
+ commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s",
+ diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
+ )
+ end
+ end
+end
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
new file mode 100644
index 00000000000..d02cfe4ec56
--- /dev/null
+++ b/app/models/integrations/hangouts_chat.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Integrations
+ class HangoutsChat < BaseChatNotification
+ include ActionView::Helpers::UrlHelper
+
+ def title
+ 'Google Chat'
+ end
+
+ def description
+ 'Send notifications from GitLab to a room in Google Chat.'
+ end
+
+ def self.to_param
+ 'hangouts_chat'
+ end
+
+ def help
+ 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)
+ end
+
+ def default_channel_placeholder
+ end
+
+ def webhook_placeholder
+ 'https://chat.googleapis.com/v1/spaces…'
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ simple_text = parse_simple_text_message(message)
+ ::HangoutsChat::Sender.new(webhook).simple(simple_text)
+ end
+
+ def parse_simple_text_message(message)
+ header = message.pretext
+ return header if message.attachments.empty?
+
+ attachment = message.attachments.first
+ title = format_attachment_title(attachment)
+ body = attachment[:text]
+
+ [header, title, body].compact.join("\n")
+ end
+
+ def format_attachment_title(attachment)
+ return attachment[:title] unless attachment[:title_link]
+
+ "<#{attachment[:title_link]}|#{attachment[:title]}>"
+ end
+ end
+end
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
new file mode 100644
index 00000000000..7048dd641ea
--- /dev/null
+++ b/app/models/integrations/irker.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'uri'
+
+module Integrations
+ class Irker < Integration
+ prop_accessor :server_host, :server_port, :default_irc_uri
+ prop_accessor :recipients, :channels
+ boolean_accessor :colorize_messages
+ validates :recipients, presence: true, if: :validate_recipients?
+
+ before_validation :get_channels
+
+ def title
+ 'Irker (IRC gateway)'
+ end
+
+ def description
+ 'Send IRC messages.'
+ end
+
+ def self.to_param
+ 'irker'
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ IrkerWorker.perform_async(project_id, channels,
+ colorize_messages, data, settings)
+ end
+
+ def settings
+ {
+ server_host: server_host.presence || 'localhost',
+ server_port: server_port.presence || 6659
+ }
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'server_host', placeholder: 'localhost',
+ help: 'Irker daemon hostname (defaults to localhost)' },
+ { type: 'text', name: 'server_port', placeholder: 6659,
+ help: 'Irker daemon port (defaults to 6659)' },
+ { type: 'text', name: 'default_irc_uri', title: 'Default IRC URI',
+ help: 'A default IRC URI to prepend before each recipient (optional)',
+ placeholder: 'irc://irc.network.net:6697/' },
+ { type: 'textarea', name: 'recipients',
+ placeholder: 'Recipients/channels separated by whitespaces', required: true,
+ help: 'Recipients have to be specified with a full URI: '\
+ 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
+ 'you want the channel to be a nickname instead, append ",isnick" to ' \
+ 'the channel name; if the channel is protected by a secret password, ' \
+ ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
+ ' want to use a password, you have to omit the "#" on the channel). If you ' \
+ ' specify a default IRC URI to prepend before each recipient, you can just ' \
+ ' give a channel name.' },
+ { type: 'checkbox', name: 'colorize_messages' }
+ ]
+ end
+
+ def help
+ ' NOTE: Irker does NOT have built-in authentication, which makes it' \
+ ' vulnerable to spamming IRC channels if it is hosted outside of a ' \
+ ' firewall. Please make sure you run the daemon within a secured network ' \
+ ' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.'
+ end
+
+ private
+
+ def get_channels
+ return true unless activated?
+ return true if recipients.nil? || recipients.empty?
+
+ map_recipients
+
+ errors.add(:recipients, 'are all invalid') if channels.empty?
+ true
+ end
+
+ def map_recipients
+ self.channels = recipients.split(/\s+/).map do |recipient|
+ format_channel(recipient)
+ end
+ channels.reject!(&:nil?)
+ end
+
+ def format_channel(recipient)
+ uri = nil
+
+ # Try to parse the chan as a full URI
+ begin
+ uri = consider_uri(URI.parse(recipient))
+ rescue URI::InvalidURIError
+ end
+
+ unless uri.present? && default_irc_uri.nil?
+ begin
+ new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
+ uri = consider_uri(URI.parse(new_recipient))
+ rescue StandardError
+ log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
+ end
+ end
+
+ uri
+ end
+
+ def consider_uri(uri)
+ return if uri.scheme.nil?
+
+ # Authorize both irc://domain.com/#chan and irc://domain.com/chan
+ if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil?
+ uri.to_s
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/issue_tracker_data.rb b/app/models/integrations/issue_tracker_data.rb
new file mode 100644
index 00000000000..8749075149f
--- /dev/null
+++ b/app/models/integrations/issue_tracker_data.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Integrations
+ class IssueTrackerData < ApplicationRecord
+ include BaseDataFields
+
+ attr_encrypted :project_url, encryption_options
+ attr_encrypted :issues_url, encryption_options
+ attr_encrypted :new_issue_url, encryption_options
+ end
+end
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
new file mode 100644
index 00000000000..815e86bcaa1
--- /dev/null
+++ b/app/models/integrations/jenkins.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Jenkins < BaseCi
+ include ActionView::Helpers::UrlHelper
+
+ prop_accessor :jenkins_url, :project_name, :username, :password
+
+ before_update :reset_password
+
+ validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
+ validates :project_name, presence: true, if: :activated?
+ validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
+
+ default_value_for :push_events, true
+ default_value_for :merge_requests_events, false
+ default_value_for :tag_push_events, false
+
+ after_save :compose_service_hook, if: :activated?
+
+ def reset_password
+ # don't reset the password if a new one is provided
+ if (jenkins_url_changed? || username.blank?) && !password_touched?
+ self.password = nil
+ end
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ service_hook.execute(data, "#{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
+
+ def hook_url
+ url = URI.parse(jenkins_url)
+ url.path = File.join(url.path || '/', "project/#{project_name}")
+ url.user = ERB::Util.url_encode(username) unless username.blank?
+ url.password = ERB::Util.url_encode(password) unless password.blank?
+ url.to_s
+ end
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def title
+ 'Jenkins'
+ end
+
+ def description
+ 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_('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
+ 'jenkins'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'jenkins_url',
+ title: s_('ProjectService|Jenkins server URL'),
+ required: true,
+ placeholder: 'http://jenkins.example.com',
+ help: s_('The URL of the Jenkins server.')
+ },
+ {
+ type: 'text',
+ name: 'project_name',
+ required: true,
+ placeholder: 'my_project_name',
+ help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
+ },
+ {
+ type: 'text',
+ name: 'username',
+ required: true,
+ help: s_('The username for the Jenkins server.')
+ },
+ {
+ type: 'password',
+ name: 'password',
+ help: s_('The password for the Jenkins server.'),
+ non_empty_password_title: s_('ProjectService|Enter new password.'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
+ }
+ ]
+ end
+ end
+end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
new file mode 100644
index 00000000000..aa143cc28e1
--- /dev/null
+++ b/app/models/integrations/jira.rb
@@ -0,0 +1,588 @@
+# frozen_string_literal: true
+
+# Accessible as Project#external_issue_tracker
+module Integrations
+ class Jira < BaseIssueTracker
+ extend ::Gitlab::Utils::Override
+ include Gitlab::Routing
+ include ApplicationHelper
+ include ActionView::Helpers::AssetUrlHelper
+ include Gitlab::Utils::StrongMemoize
+
+ PROJECTS_PER_PAGE = 50
+ JIRA_CLOUD_HOST = '.atlassian.net'
+
+ ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze
+ ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze
+
+ validates :url, public_url: true, presence: true, if: :activated?
+ validates :api_url, public_url: true, allow_blank: true
+ validates :username, presence: true, if: :activated?
+ validates :password, presence: true, if: :activated?
+
+ validates :jira_issue_transition_id,
+ format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|IDs must be a list of numbers that can be split with , or ;") },
+ allow_blank: true
+
+ # Jira Cloud version is deprecating authentication via username and password.
+ # We should use username/password for Jira Server and email/api_token for Jira Cloud,
+ # for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
+
+ # TODO: we can probably just delegate as part of
+ # https://gitlab.com/gitlab-org/gitlab/issues/29404
+ data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
+ :vulnerabilities_enabled, :vulnerabilities_issuetype
+
+ before_update :reset_password
+ after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
+
+ enum comment_detail: {
+ standard: 1,
+ all_details: 2
+ }
+
+ # When these are false GitLab does not create cross reference
+ # comments on Jira except when an issue gets transitioned.
+ def self.supported_events
+ %w(commit merge_request)
+ end
+
+ def self.supported_event_actions
+ %w(comment)
+ end
+
+ # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
+ def self.reference_pattern(only_long: true)
+ @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
+ end
+
+ def initialize_properties
+ {}
+ end
+
+ def data_fields
+ jira_tracker_data || self.build_jira_tracker_data
+ end
+
+ def reset_password
+ data_fields.password = nil if reset_password?
+ end
+
+ def set_default_data
+ return unless issues_tracker.present?
+
+ return if url
+
+ data_fields.url ||= issues_tracker['url']
+ data_fields.api_url ||= issues_tracker['api_url']
+ end
+
+ def options
+ url = URI.parse(client_url)
+
+ {
+ username: username&.strip,
+ password: password,
+ site: URI.join(url, '/').to_s.delete_suffix('/'), # Intended to find the root
+ context_path: (url.path.presence || '/').delete_suffix('/'),
+ auth_type: :basic,
+ read_timeout: 120,
+ use_cookies: true,
+ additional_cookies: ['OBBasicAuth=fromDialog'],
+ use_ssl: url.scheme == 'https'
+ }
+ end
+
+ def client
+ @client ||= begin
+ JIRA::Client.new(options).tap do |client|
+ # Replaces JIRA default http client with our implementation
+ client.request_client = Gitlab::Jira::HttpClient.new(client.options)
+ end
+ end
+ end
+
+ def help
+ 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
+ 'Jira'
+ end
+
+ def description
+ s_("JiraService|Use Jira as this project's issue tracker.")
+ end
+
+ def self.to_param
+ 'jira'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'url',
+ title: s_('JiraService|Web URL'),
+ placeholder: 'https://jira.example.com',
+ help: s_('JiraService|Base URL of the Jira instance.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'api_url',
+ title: s_('JiraService|Jira API URL'),
+ help: s_('JiraService|If different from Web URL.')
+ },
+ {
+ type: 'text',
+ name: 'username',
+ title: s_('JiraService|Username or Email'),
+ help: s_('JiraService|Use a username for server version and an email for cloud version.'),
+ required: true
+ },
+ {
+ type: 'password',
+ name: 'password',
+ title: s_('JiraService|Password or API token'),
+ non_empty_password_title: s_('JiraService|Enter new password or API token'),
+ non_empty_password_help: s_('JiraService|Leave blank to use your current password or API token.'),
+ help: s_('JiraService|Use a password for server version and an API token for cloud version.'),
+ required: true
+ }
+ ]
+ end
+
+ def web_url(path = nil, **params)
+ return unless url.present?
+
+ if Gitlab.com?
+ params.merge!(ATLASSIAN_REFERRER_GITLAB_COM) unless Gitlab.staging?
+ else
+ params.merge!(ATLASSIAN_REFERRER_SELF_MANAGED) unless Gitlab.dev_or_test_env?
+ end
+
+ url = Addressable::URI.parse(self.url)
+ url.path = url.path.delete_suffix('/')
+ url.path << "/#{path.delete_prefix('/').delete_suffix('/')}" if path.present?
+ url.query_values = (url.query_values || {}).merge(params)
+ url.query_values = nil if url.query_values.empty?
+
+ url.to_s
+ end
+
+ override :project_url
+ def project_url
+ web_url
+ end
+
+ override :issues_url
+ def issues_url
+ web_url('browse/:id')
+ end
+
+ override :new_issue_url
+ def new_issue_url
+ web_url('secure/CreateIssue!default.jspa')
+ end
+
+ alias_method :original_url, :url
+ def url
+ original_url&.delete_suffix('/')
+ end
+
+ alias_method :original_api_url, :api_url
+ def api_url
+ original_api_url&.delete_suffix('/')
+ end
+
+ def execute(push)
+ # This method is a no-op, because currently Integrations::Jira does not
+ # support any events.
+ end
+
+ def find_issue(issue_key, rendered_fields: false, transitions: false)
+ expands = []
+ expands << 'renderedFields' if rendered_fields
+ expands << 'transitions' if transitions
+ options = { expand: expands.join(',') } if expands.any?
+
+ jira_request { client.Issue.find(issue_key, options || {}) }
+ end
+
+ def close_issue(entity, external_issue, current_user)
+ issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)
+
+ return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?
+
+ commit_id = case entity
+ when Commit then entity.id
+ when MergeRequest then entity.diff_head_sha
+ end
+
+ commit_url = build_entity_url(:commit, commit_id)
+
+ # Depending on the Jira project's workflow, a comment during transition
+ # may or may not be allowed. Refresh the issue after transition and check
+ # if it is closed, so we don't have one comment for every commit.
+ issue = find_issue(issue.key) if transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
+ log_usage(:close_issue, current_user)
+ end
+
+ override :create_cross_reference_note
+ def create_cross_reference_note(mentioned, noteable, author)
+ unless can_cross_reference?(noteable)
+ return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) }
+ end
+
+ jira_issue = find_issue(mentioned.id)
+
+ return unless jira_issue.present?
+
+ noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
+ noteable_type = noteable_name(noteable)
+ entity_url = build_entity_url(noteable_type, noteable_id)
+ entity_meta = build_entity_meta(noteable)
+
+ data = {
+ user: {
+ name: author.name,
+ url: resource_url(user_path(author))
+ },
+ project: {
+ name: project.full_path,
+ url: resource_url(project_path(project))
+ },
+ entity: {
+ id: entity_meta[:id],
+ name: noteable_type.humanize.downcase,
+ url: entity_url,
+ title: noteable.title,
+ description: entity_meta[:description],
+ branch: entity_meta[:branch]
+ }
+ }
+
+ add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
+ end
+
+ def valid_connection?
+ test(nil)[:success]
+ end
+
+ def test(_)
+ result = server_info
+ success = result.present?
+ result = @error&.message unless success
+
+ { success: success, result: result }
+ end
+
+ override :support_close_issue?
+ def support_close_issue?
+ true
+ end
+
+ override :support_cross_reference?
+ def support_cross_reference?
+ true
+ end
+
+ def issue_transition_enabled?
+ jira_issue_transition_automatic || jira_issue_transition_id.present?
+ end
+
+ private
+
+ def server_info
+ strong_memoize(:server_info) do
+ client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
+ end
+ end
+
+ def can_cross_reference?(noteable)
+ case noteable
+ when Commit then commit_events
+ when MergeRequest then merge_requests_events
+ else true
+ end
+ end
+
+ # jira_issue_transition_id can have multiple values split by , or ;
+ # the issue is transitioned at the order given by the user
+ # if any transition fails it will log the error message and stop the transition sequence
+ def transition_issue(issue)
+ return transition_issue_to_done(issue) if jira_issue_transition_automatic
+
+ jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id|
+ transition_issue_to_id(issue, transition_id)
+ end
+ end
+
+ def transition_issue_to_id(issue, transition_id)
+ issue.transitions.build.save!(
+ transition: { id: transition_id }
+ )
+
+ true
+ rescue StandardError => error
+ log_error(
+ "Issue transition failed",
+ error: {
+ exception_class: error.class.name,
+ exception_message: error.message,
+ exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
+ },
+ client_url: client_url
+ )
+
+ false
+ end
+
+ def transition_issue_to_done(issue)
+ transitions = issue.transitions rescue []
+
+ transition = transitions.find do |transition|
+ status = transition&.to&.statusCategory
+ status && status['key'] == 'done'
+ end
+
+ return false unless transition
+
+ transition_issue_to_id(issue, transition.id)
+ end
+
+ def log_usage(action, user)
+ key = "i_ecosystem_jira_service_#{action}"
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id)
+ end
+
+ def add_issue_solved_comment(issue, commit_id, commit_url)
+ link_title = "Solved by commit #{commit_id}."
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
+ send_message(issue, comment, link_props)
+ end
+
+ def add_comment(data, issue)
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
+ entity_title = data[:entity][:title]
+
+ message = comment_message(data)
+ link_title = "#{entity_name.capitalize} - #{entity_title}"
+ link_props = build_remote_link_props(url: entity_url, title: link_title)
+
+ unless comment_exists?(issue, message)
+ send_message(issue, message, link_props)
+ end
+ end
+
+ def comment_message(data)
+ user_link = build_jira_link(data[:user][:name], data[:user][:url])
+
+ entity = data[:entity]
+ entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}"
+ entity_link = build_jira_link(entity_ref, entity[:url])
+
+ project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
+ branch =
+ if entity[:branch].present?
+ s_('JiraService| on branch %{branch_link}') % {
+ branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
+ }
+ end
+
+ entity_message = entity[:description].presence if all_details?
+ entity_message ||= entity[:title].chomp
+
+ s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
+ user_link: user_link,
+ entity_link: entity_link,
+ project_link: project_link,
+ branch: branch,
+ entity_message: entity_message
+ }
+ end
+
+ def build_jira_link(title, url)
+ "[#{title}|#{url}]"
+ end
+
+ def has_resolution?(issue)
+ issue.respond_to?(:resolution) && issue.resolution.present?
+ end
+
+ def comment_exists?(issue, message)
+ comments = jira_request { issue.comments }
+
+ comments.present? && comments.any? { |comment| comment.body.include?(message) }
+ end
+
+ def send_message(issue, message, remote_link_props)
+ return unless client_url.present?
+
+ jira_request do
+ remote_link = find_remote_link(issue, remote_link_props[:object][:url])
+
+ create_issue_comment(issue, message) unless remote_link
+ remote_link ||= issue.remotelink.build
+ remote_link.save!(remote_link_props)
+
+ log_info("Successfully posted", client_url: client_url)
+ "SUCCESS: Successfully posted to #{client_url}."
+ end
+ end
+
+ def create_issue_comment(issue, message)
+ return unless comment_on_event_enabled
+
+ issue.comments.build.save!(body: message)
+ end
+
+ def find_remote_link(issue, url)
+ links = jira_request { issue.remotelink.all }
+ return unless links
+
+ links.find { |link| link.object["url"] == url }
+ end
+
+ def build_remote_link_props(url:, title:, resolved: false)
+ status = {
+ resolved: resolved
+ }
+
+ {
+ GlobalID: 'GitLab',
+ relationship: 'mentioned on',
+ object: {
+ url: url,
+ title: title,
+ status: status,
+ icon: {
+ title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.base_url)
+ }
+ }
+ }
+ end
+
+ def resource_url(resource)
+ "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
+ end
+
+ def build_entity_url(noteable_type, entity_id)
+ polymorphic_url(
+ [
+ self.project,
+ noteable_type.to_sym
+ ],
+ id: entity_id,
+ host: Settings.gitlab.base_url
+ )
+ end
+
+ def build_entity_meta(noteable)
+ if noteable.is_a?(Commit)
+ {
+ id: noteable.short_id,
+ description: noteable.safe_message,
+ branch: noteable.ref_names(project.repository).first
+ }
+ elsif noteable.is_a?(MergeRequest)
+ {
+ id: noteable.to_reference,
+ branch: noteable.source_branch
+ }
+ else
+ {}
+ end
+ end
+
+ def noteable_name(noteable)
+ name = noteable.model_name.singular
+
+ # ProjectSnippet inherits from Snippet class so it causes
+ # routing error building the URL.
+ name == "project_snippet" ? "snippet" : name
+ end
+
+ # Handle errors when doing Jira API calls
+ def jira_request
+ yield
+ rescue StandardError => error
+ @error = error
+ log_error("Error sending message", client_url: client_url, error: @error.message)
+ nil
+ end
+
+ def client_url
+ api_url.presence || url
+ end
+
+ def reset_password?
+ # don't reset the password if a new one is provided
+ return false if password_touched?
+ return true if api_url_changed?
+ return false if api_url.present?
+
+ url_changed?
+ end
+
+ def update_deployment_type?
+ (api_url_changed? || url_changed? || username_changed? || password_changed?) &&
+ can_test?
+ end
+
+ def update_deployment_type
+ clear_memoization(:server_info) # ensure we run the request when we try to update deployment type
+ results = server_info
+
+ unless results.present?
+ Gitlab::AppLogger.warn(message: "Jira API returned no ServerInfo, setting deployment_type from URL", server_info: results, url: client_url)
+
+ return set_deployment_type_from_url
+ end
+
+ if jira_cloud?
+ data_fields.deployment_cloud!
+ else
+ data_fields.deployment_server!
+ end
+ end
+
+ def jira_cloud?
+ server_info['deploymentType'] == 'Cloud' || URI(client_url).hostname.end_with?(JIRA_CLOUD_HOST)
+ end
+
+ def set_deployment_type_from_url
+ # This shouldn't happen but of course it will happen when an integration is removed.
+ # Instead of deleting the integration we set all fields to null
+ # and mark it as inactive
+ return data_fields.deployment_unknown! unless client_url
+
+ # If API-based detection methods fail here then
+ # we can only assume it's either Cloud or Server
+ # based on the URL being *.atlassian.net
+
+ if URI(client_url).hostname.end_with?(JIRA_CLOUD_HOST)
+ data_fields.deployment_cloud!
+ else
+ data_fields.deployment_server!
+ end
+ end
+
+ def self.event_description(event)
+ case event
+ when "merge_request", "merge_request_events"
+ s_("JiraService|Jira comments are created when an issue is referenced in a merge request.")
+ when "commit", "commit_events"
+ s_("JiraService|Jira comments are created when an issue is referenced in a commit.")
+ end
+ end
+ end
+end
+
+Integrations::Jira.prepend_mod_with('Integrations::Jira')
diff --git a/app/models/integrations/jira_tracker_data.rb b/app/models/integrations/jira_tracker_data.rb
new file mode 100644
index 00000000000..74352393b43
--- /dev/null
+++ b/app/models/integrations/jira_tracker_data.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Integrations
+ class JiraTrackerData < ApplicationRecord
+ include BaseDataFields
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
+
+ enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment
+ end
+end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
new file mode 100644
index 00000000000..07a5086b8e9
--- /dev/null
+++ b/app/models/integrations/mattermost.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Mattermost < BaseChatNotification
+ include SlackMattermostNotifier
+ include ActionView::Helpers::UrlHelper
+
+ def title
+ s_('Mattermost notifications')
+ end
+
+ def description
+ s_('Send notifications about project events to Mattermost channels.')
+ end
+
+ def self.to_param
+ 'mattermost'
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def default_channel_placeholder
+ 'my-channel'
+ end
+
+ def webhook_placeholder
+ 'http://mattermost.example.com/hooks/'
+ end
+ end
+end
diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb
new file mode 100644
index 00000000000..6cd664da9e7
--- /dev/null
+++ b/app/models/integrations/mattermost_slash_commands.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Integrations
+ class MattermostSlashCommands < BaseSlashCommands
+ include Ci::TriggersHelper
+
+ prop_accessor :token
+
+ def can_test?
+ false
+ end
+
+ def title
+ 'Mattermost slash commands'
+ end
+
+ def description
+ "Perform common tasks with slash commands."
+ end
+
+ def self.to_param
+ 'mattermost_slash_commands'
+ end
+
+ def configure(user, params)
+ token = ::Mattermost::Command.new(user)
+ .create(command(params))
+
+ update(active: true, token: token) if token
+ rescue ::Mattermost::Error => e
+ [false, e.message]
+ end
+
+ def list_teams(current_user)
+ [::Mattermost::Team.new(current_user).all, nil]
+ rescue ::Mattermost::Error => e
+ [[], e.message]
+ end
+
+ def chat_responder
+ ::Gitlab::Chat::Responder::Mattermost
+ end
+
+ private
+
+ def command(params)
+ pretty_project_name = project.full_name
+
+ params.merge(
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{pretty_project_name}",
+ display_name: "GitLab / #{pretty_project_name}",
+ method: 'P',
+ username: 'GitLab')
+ end
+ end
+end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
new file mode 100644
index 00000000000..91e6800f03c
--- /dev/null
+++ b/app/models/integrations/microsoft_teams.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Integrations
+ class MicrosoftTeams < BaseChatNotification
+ def title
+ 'Microsoft Teams notifications'
+ end
+
+ def description
+ 'Send notifications about project events to Microsoft Teams.'
+ end
+
+ def self.to_param
+ 'microsoft_teams'
+ end
+
+ def help
+ '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>'
+ end
+
+ def webhook_placeholder
+ 'https://outlook.office.com/webhook/…'
+ end
+
+ def event_field(event)
+ end
+
+ def default_channel_placeholder
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ ::MicrosoftTeams::Notifier.new(webhook).ping(
+ title: message.project_name,
+ summary: message.summary,
+ activity: message.activity,
+ attachments: message.attachments
+ )
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+ end
+end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
new file mode 100644
index 00000000000..d31f6381767
--- /dev/null
+++ b/app/models/integrations/mock_ci.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
+module Integrations
+ class MockCi < BaseCi
+ ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
+
+ prop_accessor :mock_service_url
+ validates :mock_service_url, presence: true, public_url: true, if: :activated?
+
+ def title
+ 'MockCI'
+ end
+
+ def description
+ 'Mock an external CI'
+ end
+
+ def self.to_param
+ 'mock_ci'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'mock_service_url',
+ title: s_('ProjectService|Mock service URL'),
+ placeholder: 'http://localhost:4004',
+ required: true
+ }
+ ]
+ end
+
+ # Return complete url to build page
+ #
+ # Ex.
+ # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
+ #
+ def build_page(sha, ref)
+ Gitlab::Utils.append_path(
+ mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}")
+ end
+
+ # Return string with build status or :error symbol
+ #
+ # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
+ #
+ # Ex.
+ # @service.commit_status('13be4ac', 'master')
+ # # => 'success'
+ #
+ # @service.commit_status('2abe4ac', 'dev')
+ # # => 'running'
+ #
+ def commit_status(sha, ref)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
+ read_commit_status(response)
+ rescue Errno::ECONNREFUSED
+ :error
+ end
+
+ def commit_status_path(sha)
+ Gitlab::Utils.append_path(
+ mock_service_url,
+ "#{project.namespace.path}/#{project.path}/status/#{sha}.json")
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'pending'
+ else
+ response['status']
+ end
+
+ if status.present? && ALLOWED_STATES.include?(status)
+ status
+ else
+ :error
+ end
+ end
+
+ def can_test?
+ false
+ end
+ end
+end
diff --git a/app/models/integrations/open_project.rb b/app/models/integrations/open_project.rb
new file mode 100644
index 00000000000..e4cfb24151a
--- /dev/null
+++ b/app/models/integrations/open_project.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Integrations
+ class OpenProject < BaseIssueTracker
+ validates :url, public_url: true, presence: true, if: :activated?
+ validates :api_url, public_url: true, allow_blank: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+ validates :project_identifier_code, presence: true, if: :activated?
+
+ data_field :url, :api_url, :token, :closed_status_id, :project_identifier_code
+
+ def data_fields
+ open_project_tracker_data || self.build_open_project_tracker_data
+ end
+
+ def self.to_param
+ 'open_project'
+ end
+ end
+end
diff --git a/app/models/integrations/open_project_tracker_data.rb b/app/models/integrations/open_project_tracker_data.rb
new file mode 100644
index 00000000000..b3f2618b94f
--- /dev/null
+++ b/app/models/integrations/open_project_tracker_data.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Integrations
+ class OpenProjectTrackerData < ApplicationRecord
+ include BaseDataFields
+
+ # When the Open Project is fresh installed, the default closed status id is "13" based on current version: v8.
+ DEFAULT_CLOSED_STATUS_ID = "13"
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :token, encryption_options
+
+ def closed_status_id
+ super || DEFAULT_CLOSED_STATUS_ID
+ end
+ end
+end
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
new file mode 100644
index 00000000000..b597bd11175
--- /dev/null
+++ b/app/models/integrations/packagist.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Packagist < Integration
+ prop_accessor :username, :token, :server
+
+ validates :username, presence: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+
+ default_value_for :push_events, true
+ default_value_for :tag_push_events, true
+
+ after_save :compose_service_hook, if: :activated?
+
+ def title
+ 'Packagist'
+ end
+
+ def description
+ s_('Integrations|Update your Packagist projects.')
+ end
+
+ def self.to_param
+ 'packagist'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
+ ]
+ end
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ service_hook.execute(data)
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ return { success: false, result: result[:message] } if result[:http_status] != 202
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result[:message] }
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def hook_url
+ base_url = server.presence || 'https://packagist.org'
+ "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}"
+ end
+ end
+end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
new file mode 100644
index 00000000000..585bc14242a
--- /dev/null
+++ b/app/models/integrations/pipelines_email.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Integrations
+ class PipelinesEmail < Integration
+ include NotificationBranchSelection
+
+ prop_accessor :recipients, :branches_to_be_notified
+ boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
+ validates :recipients, presence: true, if: :validate_recipients?
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_pipelines = true
+ self.branches_to_be_notified = "default"
+ 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
+ # `branches_to_be_notified`. Instead of doing a background migration, we
+ # opted to set a value for the new property based on the old one, if
+ # users hasn't specified one already. When users edit the service and
+ # selects a value for this new property, it will override everything.
+
+ self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
+ end
+ end
+
+ def title
+ _('Pipeline status emails')
+ end
+
+ def description
+ _('Email the pipeline status to a list of recipients.')
+ end
+
+ def self.to_param
+ 'pipelines_email'
+ end
+
+ def self.supported_events
+ %w[pipeline]
+ end
+
+ def self.default_test_event
+ 'pipeline'
+ end
+
+ def execute(data, force: false)
+ return unless supported_events.include?(data[:object_kind])
+ return unless force || should_pipeline_be_notified?(data)
+
+ all_recipients = retrieve_recipients(data)
+
+ return unless all_recipients.any?
+
+ pipeline_id = data[:object_attributes][:id]
+ PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients)
+ end
+
+ def can_test?
+ project&.ci_pipelines&.any?
+ end
+
+ def fields
+ [
+ { type: 'textarea',
+ name: 'recipients',
+ help: _('Comma-separated list of email addresses.'),
+ required: true },
+ { type: 'checkbox',
+ name: 'notify_only_broken_pipelines' },
+ { type: 'select',
+ name: 'branches_to_be_notified',
+ choices: branch_choices }
+ ]
+ end
+
+ def test(data)
+ result = execute(data, force: true)
+
+ { success: true, result: result }
+ rescue StandardError => error
+ { success: false, result: error }
+ end
+
+ def should_pipeline_be_notified?(data)
+ notify_for_branch?(data) && notify_for_pipeline?(data)
+ end
+
+ def notify_for_pipeline?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+
+ def retrieve_recipients(data)
+ recipients.to_s.split(/[,\r\n ]+/).reject(&:empty?)
+ end
+ end
+end
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
new file mode 100644
index 00000000000..46f97cc3c6b
--- /dev/null
+++ b/app/models/integrations/pivotaltracker.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Pivotaltracker < Integration
+ API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+
+ prop_accessor :token, :restrict_to_branch
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'PivotalTracker'
+ end
+
+ def description
+ s_('PivotalTrackerService|Add commit messages as comments to PivotalTracker stories.')
+ end
+
+ def self.to_param
+ 'pivotaltracker'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'token',
+ placeholder: s_('PivotalTrackerService|Pivotal Tracker API token.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ placeholder: s_('PivotalTrackerService|Comma-separated list of branches which will be ' \
+ 'automatically inspected. Leave blank to include all branches.')
+ }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+ return unless allowed_branch?(data[:ref])
+
+ data[:commits].each do |commit|
+ message = {
+ 'source_commit' => {
+ 'commit_id' => commit[:id],
+ 'author' => commit[:author][:name],
+ 'url' => commit[:url],
+ 'message' => commit[:message]
+ }
+ }
+ Gitlab::HTTP.post(
+ API_ENDPOINT,
+ body: message.to_json,
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'X-TrackerToken' => token
+ }
+ )
+ end
+ end
+
+ private
+
+ def allowed_branch?(ref)
+ return true unless ref.present? && restrict_to_branch.present?
+
+ branch = Gitlab::Git.ref_name(ref)
+ allowed_branches = restrict_to_branch.split(',').map(&:strip)
+
+ branch.present? && allowed_branches.include?(branch)
+ end
+ end
+end
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
new file mode 100644
index 00000000000..b0cadc7ef4e
--- /dev/null
+++ b/app/models/integrations/pushover.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Pushover < Integration
+ BASE_URI = 'https://api.pushover.net/1'
+
+ prop_accessor :api_key, :user_key, :device, :priority, :sound
+ validates :api_key, :user_key, :priority, presence: true, if: :activated?
+
+ def title
+ 'Pushover'
+ end
+
+ def description
+ s_('PushoverService|Get real-time notifications on your device.')
+ end
+
+ def self.to_param
+ 'pushover'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'api_key', title: _('API key'), placeholder: s_('PushoverService|Your application key'), required: true },
+ { type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true },
+ { type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') },
+ { type: 'select', name: 'priority', required: true, choices:
+ [
+ [s_('PushoverService|Lowest Priority'), -2],
+ [s_('PushoverService|Low Priority'), -1],
+ [s_('PushoverService|Normal Priority'), 0],
+ [s_('PushoverService|High Priority'), 1]
+ ],
+ default_choice: 0 },
+ { type: 'select', name: 'sound', choices:
+ [
+ ['Device default sound', nil],
+ ['Pushover (default)', 'pushover'],
+ %w(Bike bike),
+ %w(Bugle bugle),
+ ['Cash Register', 'cashregister'],
+ %w(Classical classical),
+ %w(Cosmic cosmic),
+ %w(Falling falling),
+ %w(Gamelan gamelan),
+ %w(Incoming incoming),
+ %w(Intermission intermission),
+ %w(Magic magic),
+ %w(Mechanical mechanical),
+ ['Piano Bar', 'pianobar'],
+ %w(Siren siren),
+ ['Space Alarm', 'spacealarm'],
+ ['Tug Boat', 'tugboat'],
+ ['Alien Alarm (long)', 'alien'],
+ ['Climb (long)', 'climb'],
+ ['Persistent (long)', 'persistent'],
+ ['Pushover Echo (long)', 'echo'],
+ ['Up Down (long)', 'updown'],
+ ['None (silent)', 'none']
+ ] }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ ref = Gitlab::Git.ref_name(data[:ref])
+ before = data[:before]
+ after = data[:after]
+
+ message =
+ if Gitlab::Git.blank_ref?(before)
+ s_("PushoverService|%{user_name} pushed new branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref }
+ elsif Gitlab::Git.blank_ref?(after)
+ s_("PushoverService|%{user_name} deleted branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref }
+ else
+ s_("PushoverService|%{user_name} push to branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref }
+ end
+
+ if data[:total_commits_count] > 0
+ message = [message, s_("PushoverService|Total commits count: %{total_commits_count}") % { total_commits_count: data[:total_commits_count] }].join("\n")
+ end
+
+ pushover_data = {
+ token: api_key,
+ user: user_key,
+ device: device,
+ priority: priority,
+ title: "#{project.full_name}",
+ message: message,
+ url: data[:project][:web_url],
+ url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name }
+ }
+
+ # Sound parameter MUST NOT be sent to API if not selected
+ if sound
+ pushover_data[:sound] = sound
+ end
+
+ Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
+ end
+ end
+end
diff --git a/app/models/integrations/redmine.rb b/app/models/integrations/redmine.rb
new file mode 100644
index 00000000000..990b538f294
--- /dev/null
+++ b/app/models/integrations/redmine.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Redmine < BaseIssueTracker
+ include ActionView::Helpers::UrlHelper
+ validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
+
+ def title
+ 'Redmine'
+ end
+
+ def description
+ s_("IssueTracker|Use Redmine 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/redmine'), target: '_blank', rel: 'noopener noreferrer'
+ s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'redmine'
+ end
+ end
+end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
new file mode 100644
index 00000000000..0381db3a67e
--- /dev/null
+++ b/app/models/integrations/slack.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Slack < BaseChatNotification
+ include SlackMattermostNotifier
+ extend ::Gitlab::Utils::Override
+
+ SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
+ push issue confidential_issue merge_request note confidential_note
+ tag_push wiki_page deployment
+ ].freeze
+
+ prop_accessor EVENT_CHANNEL['alert']
+
+ def title
+ 'Slack notifications'
+ end
+
+ def description
+ 'Send notifications about project events to Slack.'
+ end
+
+ def self.to_param
+ 'slack'
+ end
+
+ def default_channel_placeholder
+ _('#general, #development')
+ end
+
+ def webhook_placeholder
+ 'https://hooks.slack.com/services/…'
+ end
+
+ def supported_events
+ additional = []
+ additional << 'alert'
+
+ super + additional
+ end
+
+ def get_message(object_kind, data)
+ return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+
+ super
+ end
+
+ override :log_usage
+ def log_usage(event, user_id)
+ return unless user_id
+
+ return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
+
+ key = "i_ecosystem_slack_service_#{event}_notification"
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
+ end
+ end
+end
diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb
new file mode 100644
index 00000000000..ff1f806df45
--- /dev/null
+++ b/app/models/integrations/slack_slash_commands.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Integrations
+ class SlackSlashCommands < BaseSlashCommands
+ include Ci::TriggersHelper
+
+ def title
+ 'Slack slash commands'
+ end
+
+ def description
+ "Perform common operations in Slack"
+ end
+
+ def self.to_param
+ 'slack_slash_commands'
+ end
+
+ def trigger(params)
+ # Format messages to be Slack-compatible
+ super.tap do |result|
+ result[:text] = format(result[:text]) if result.is_a?(Hash)
+ end
+ end
+
+ def chat_responder
+ ::Gitlab::Chat::Responder::Slack
+ end
+
+ private
+
+ def format(text)
+ ::Slack::Messenger::Util::LinkFormatter.format(text) if text
+ end
+ end
+end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
new file mode 100644
index 00000000000..8284d5963ae
--- /dev/null
+++ b/app/models/integrations/teamcity.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Teamcity < BaseCi
+ include ReactiveService
+ include ServicePushDataValidations
+
+ prop_accessor :teamcity_url, :build_type, :username, :password
+
+ validates :teamcity_url, presence: true, public_url: true, if: :activated?
+ validates :build_type, 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
+
+ class << self
+ def to_param
+ 'teamcity'
+ end
+
+ def supported_events
+ %w(push merge_request)
+ end
+
+ def event_description(event)
+ case event
+ when 'push', 'push_events'
+ 'TeamCity CI will be triggered after every push to the repository except branch delete'
+ when 'merge_request', 'merge_request_events'
+ 'TeamCity CI will be triggered after a merge request has been created or updated'
+ end
+ end
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.save
+ end
+
+ def reset_password
+ if teamcity_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
+
+ def title
+ 'JetBrains TeamCity'
+ end
+
+ def description
+ s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
+ end
+
+ def help
+ s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'teamcity_url',
+ title: s_('ProjectService|TeamCity server URL'),
+ placeholder: 'https://teamcity.example.com',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_type',
+ help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
+ },
+ {
+ 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 calculate_reactive_cache(sha, ref)
+ response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}")
+
+ if response
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ else
+ { build_page: teamcity_url, commit_status: :error }
+ end
+ end
+
+ def execute(data)
+ case data[:object_kind]
+ when 'push'
+ execute_push(data)
+ when 'merge_request'
+ execute_merge_request(data)
+ end
+ end
+
+ private
+
+ def execute_push(data)
+ branch = Gitlab::Git.ref_name(data[:ref])
+ post_to_build_queue(data, branch) if push_valid?(data)
+ end
+
+ def execute_merge_request(data)
+ branch = data[:object_attributes][:source_branch]
+ post_to_build_queue(data, branch) if merge_request_valid?(data)
+ end
+
+ def read_build_page(response)
+ if response.code != 200
+ # If actual build link can't be determined,
+ # send user to build summary page.
+ build_url("viewLog.html?buildTypeId=#{build_type}")
+ else
+ # If actual build link is available, go to build result page.
+ built_id = response['build']['id']
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
+ end
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'Pending'
+ else
+ response['build']['status']
+ end
+
+ return :error unless status.present?
+
+ if status.include?('SUCCESS')
+ 'success'
+ elsif status.include?('FAILURE')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
+ def build_url(path)
+ Gitlab::Utils.append_path(teamcity_url, path)
+ end
+
+ def get_path(path)
+ Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
+ end
+
+ def post_to_build_queue(data, branch)
+ Gitlab::HTTP.post(
+ build_url('httpAuth/app/rest/buildQueue'),
+ body: "<build branchName=#{branch.encode(xml: :attr)}>"\
+ "<buildType id=#{build_type.encode(xml: :attr)}/>"\
+ '</build>',
+ headers: { 'Content-type' => 'application/xml' },
+ basic_auth: basic_auth
+ )
+ end
+
+ def basic_auth
+ { username: username, password: password }
+ end
+ end
+end
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
new file mode 100644
index 00000000000..03363c7c8b0
--- /dev/null
+++ b/app/models/integrations/unify_circuit.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Integrations
+ class UnifyCircuit < BaseChatNotification
+ def title
+ 'Unify Circuit'
+ end
+
+ def description
+ s_('Integrations|Send notifications about project events to Unify Circuit.')
+ end
+
+ def self.to_param
+ 'unify_circuit'
+ end
+
+ def help
+ 'This service sends notifications about projects events to a Unify Circuit conversation.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">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>'
+ end
+
+ def event_field(event)
+ end
+
+ def default_channel_placeholder
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ response = Gitlab::HTTP.post(webhook, body: {
+ subject: message.project_name,
+ text: message.summary,
+ markdown: true
+ }.to_json)
+
+ response if response.success?
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+ end
+end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
new file mode 100644
index 00000000000..3f420331035
--- /dev/null
+++ b/app/models/integrations/webex_teams.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Integrations
+ class WebexTeams < BaseChatNotification
+ include ActionView::Helpers::UrlHelper
+
+ def title
+ s_("WebexTeamsService|Webex Teams")
+ end
+
+ def description
+ s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
+ end
+
+ def self.to_param
+ 'webex_teams'
+ end
+
+ def help
+ 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)
+ end
+
+ def default_channel_placeholder
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { 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 }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ header = { 'Content-Type' => 'application/json' }
+ response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
+
+ response if response.success?
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+ end
+end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
new file mode 100644
index 00000000000..10531717f11
--- /dev/null
+++ b/app/models/integrations/youtrack.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Youtrack < BaseIssueTracker
+ 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
+ def self.reference_pattern(only_long: false)
+ if only_long
+ /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)/
+ else
+ /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})/
+ end
+ end
+
+ def title
+ 'YouTrack'
+ end
+
+ def description
+ 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
+ 'youtrack'
+ end
+
+ def fields
+ [
+ { 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
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2077f9bfdbb..b0a126c4442 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -464,6 +464,10 @@ class Issue < ApplicationRecord
issue_type_supports?(:assignee)
end
+ def supports_time_tracking?
+ issue_type_supports?(:time_tracking)
+ end
+
def email_participants_emails
issue_email_participants.pluck(:email)
end
@@ -524,7 +528,7 @@ class Issue < ApplicationRecord
def could_not_move(exception)
# Symptom of running out of space - schedule rebalancing
- IssueRebalancingWorker.perform_async(nil, project_id)
+ IssueRebalancingWorker.perform_async(nil, *project.self_or_root_group_ids)
end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 15b3c460b52..64385953865 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -7,6 +7,7 @@ class Key < ApplicationRecord
include Sortable
include Sha256Attribute
include Expirable
+ include FromUnion
sha256_attribute :fingerprint_sha256
@@ -43,7 +44,9 @@ class Key < ApplicationRecord
scope :preload_users, -> { preload(:user) }
scope :for_user, -> (user) { where(user: user) }
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
- scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
+
+ # Date is set specifically in this scope to improve query time.
+ scope :expired_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') BETWEEN '2000-01-01' AND CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
scope :expiring_soon_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') > CURRENT_DATE AND date(expires_at AT TIME ZONE 'UTC') < ? AND before_expiry_notification_delivered_at IS NULL", DAYS_TO_EXPIRE.days.from_now.to_date]) }
def self.regular_keys
diff --git a/app/models/label.rb b/app/models/label.rb
index a46d6bc5c0f..1a07620f944 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -9,6 +9,10 @@ class Label < ApplicationRecord
include Sortable
include FromUnion
include Presentable
+ include IgnorableColumns
+
+ # TODO: Project#create_labels can remove column exception when this column is dropped from all envs
+ ignore_column :remove_on_close, remove_with: '14.1', remove_after: '2021-06-22'
cache_markdown_field :description, pipeline: :single_line
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index a466fe69300..4fb5fd8c58a 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -11,5 +11,4 @@ class LabelLink < ApplicationRecord
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/lfs_object.rb b/app/models/lfs_object.rb
index b837b902e2d..53e7d52c558 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -7,7 +7,7 @@ class LfsObject < ApplicationRecord
include ObjectStorage::BackgroundMove
include FileStoreMounter
- has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :lfs_objects_projects
has_many :projects, -> { distinct }, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) }
@@ -18,6 +18,8 @@ class LfsObject < ApplicationRecord
mount_file_store_uploader LfsObjectUploader
+ BATCH_SIZE = 3000
+
def self.not_linked_to_project(project)
where('NOT EXISTS (?)',
project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
@@ -37,13 +39,14 @@ class LfsObject < ApplicationRecord
file_store == LfsObjectUploader::Store::LOCAL
end
- # rubocop: disable Cop/DestroyAll
- def self.destroy_unreferenced
- joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
- .where(lfs_objects_projects: { id: nil })
- .destroy_all
+ def self.unreferenced_in_batches
+ each_batch(of: BATCH_SIZE, order: :desc) do |lfs_objects|
+ relation = lfs_objects.where('NOT EXISTS (?)',
+ LfsObjectsProject.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
+
+ yield relation if relation.any?
+ end
end
- # rubocop: enable Cop/DestroyAll
def self.calculate_oid(path)
self.hexdigest(path)
diff --git a/app/models/member.rb b/app/models/member.rb
index 044b662e10f..0636c3c2d4e 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -14,6 +14,7 @@ class Member < ApplicationRecord
include UpdateHighestRole
AVATAR_SIZE = 40
+ ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
attr_accessor :raw_invite_token
@@ -107,10 +108,14 @@ class Member < ApplicationRecord
scope :active_without_invites_and_requests, -> do
left_join_users
.where(users: { state: 'active' })
- .non_request
+ .without_invites_and_requests
+ .reorder(nil)
+ end
+
+ scope :without_invites_and_requests, -> do
+ non_request
.non_invite
.non_minimal_access
- .reorder(nil)
end
scope :invite, -> { where.not(invite_token: nil) }
@@ -166,10 +171,10 @@ class Member < ApplicationRecord
after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
- after_create :post_create_hook, unless: [:pending?, :importing?]
- after_update :post_update_hook, unless: [:pending?, :importing?]
+ after_create :post_create_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
+ after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
after_destroy :destroy_notification_setting
- after_destroy :post_destroy_hook, unless: :pending?
+ after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
after_commit :refresh_member_authorized_projects
default_value_for :notification_level, NotificationSetting.levels[:global]
@@ -336,7 +341,7 @@ class Member < ApplicationRecord
return User.find_by(id: user) if user.is_a?(Integer)
- User.find_by(email: user) || user
+ User.find_by_any_email(user) || user
end
def retrieve_member(source, user, existing_members)
@@ -383,6 +388,12 @@ class Member < ApplicationRecord
invite? || request?
end
+ def hook_prerequisites_met?
+ # It is essential that an associated user record exists
+ # so that we can successfully fire any member related hooks/notifications.
+ user.present?
+ end
+
def accept_request
return false unless request?
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index b22a4fa9ef6..c7bc31cde5d 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -8,7 +8,7 @@ class GroupMember < Member
belongs_to :group, foreign_key: 'source_id'
alias_attribute :namespace_id, :source_id
- delegate :update_two_factor_requirement, to: :user
+ delegate :update_two_factor_requirement, to: :user, allow_nil: true
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
@@ -36,6 +36,10 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner
end
+ def self.pluck_user_ids
+ pluck(:user_id)
+ end
+
def group
source
end
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
index 64decb1df36..dcf0a2d0ad3 100644
--- a/app/models/members/last_group_owner_assigner.rb
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -1,46 +1,44 @@
# frozen_string_literal: true
-module Members
- class LastGroupOwnerAssigner
- def initialize(group, members)
- @group = group
- @members = members
- end
+class LastGroupOwnerAssigner
+ def initialize(group, members)
+ @group = group
+ @members = members
+ end
- def execute
- @last_blocked_owner = no_owners_in_heirarchy? && group.single_blocked_owner?
- @group_single_owner = owners.size == 1
+ def execute
+ @last_blocked_owner = no_owners_in_heirarchy? && group.single_blocked_owner?
+ @group_single_owner = owners.size == 1
- members.each { |member| set_last_owner(member) }
- end
+ members.each { |member| set_last_owner(member) }
+ end
- private
+ private
- attr_reader :group, :members, :last_blocked_owner, :group_single_owner
+ attr_reader :group, :members, :last_blocked_owner, :group_single_owner
- def no_owners_in_heirarchy?
- owners.empty?
- end
+ def no_owners_in_heirarchy?
+ owners.empty?
+ end
- def set_last_owner(member)
- member.last_owner = member.id.in?(owner_ids) && group_single_owner
- member.last_blocked_owner = member.id.in?(blocked_owner_ids) && last_blocked_owner
- end
+ def set_last_owner(member)
+ member.last_owner = member.id.in?(owner_ids) && group_single_owner
+ member.last_blocked_owner = member.id.in?(blocked_owner_ids) && last_blocked_owner
+ end
- def owner_ids
- @owner_ids ||= owners.where(id: member_ids).ids
- end
+ def owner_ids
+ @owner_ids ||= owners.where(id: member_ids).ids
+ end
- def blocked_owner_ids
- @blocked_owner_ids ||= group.blocked_owners.where(id: member_ids).ids
- end
+ def blocked_owner_ids
+ @blocked_owner_ids ||= group.blocked_owners.where(id: member_ids).ids
+ end
- def member_ids
- @members_ids ||= members.pluck(:id)
- end
+ def member_ids
+ @members_ids ||= members.pluck(:id)
+ end
- def owners
- @owners ||= group.members_with_parents.owners.load
- end
+ def owners
+ @owners ||= group.members_with_parents.owners.load
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index aaef56418d2..15f112690d5 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -37,7 +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::CompareMetricsReportsService' => ->(project) { true },
'Ci::CompareCodequalityReportsService' => ->(project) { true }
}.freeze
@@ -125,6 +125,8 @@ class MergeRequest < ApplicationRecord
].freeze
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
+ before_validation :set_draft_status
+
after_create :ensure_merge_request_diff
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
@@ -267,6 +269,7 @@ class MergeRequest < ApplicationRecord
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :open_and_closed, -> { with_states(:opened, :closed) }
+ scope :drafts, -> { where(draft: true) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
scope :by_commit_sha, ->(sha) do
where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
@@ -1908,6 +1911,10 @@ class MergeRequest < ApplicationRecord
private
+ def set_draft_status
+ self.draft = draft?
+ end
+
def missing_report_error(report_type)
{ status: :error, status_reason: "This merge request does not have #{report_type} reports" }
end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index e081a96dc10..0f2a7515462 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -12,7 +12,7 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
- serialize :trailers, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
validates :trailers, json_schema: { filename: 'git_trailers' }
# Sort by committed date in descending order to ensure latest commits comes on the top
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 2dc6796732f..f58d7788432 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -389,11 +389,23 @@ class MergeRequestDiff < ApplicationRecord
def diffs_in_batch(batch_page, batch_size, diff_options:)
fetching_repository_diffs(diff_options) do |comparison|
+ reorder_diff_files!
+ diffs_batch = diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options)
+
if comparison
- comparison.diffs_in_batch(batch_page, batch_size, diff_options: diff_options)
+ if diff_options[:paths].blank? && !without_files?
+ # Return the empty MergeRequestDiffBatch for an out of bound batch request
+ break diffs_batch if diffs_batch.diff_file_paths.blank?
+
+ diff_options.merge!(
+ paths: diffs_batch.diff_file_paths,
+ pagination_data: diffs_batch.pagination_data
+ )
+ end
+
+ comparison.diffs(diff_options)
else
- reorder_diff_files!
- diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options)
+ diffs_batch
end
end
end
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 259690ef308..ed398e0d2e0 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -12,7 +12,7 @@ class MergeRequestDiffCommit < ApplicationRecord
sha_attribute :sha
alias_attribute :id, :sha
- serialize :trailers, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
validates :trailers, json_schema: { filename: 'git_trailers' }
# Deprecated; use `bulk_insert!` from `BulkInsertSafe` mixin instead.
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 16090f0ebfa..9ed6c106e45 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -36,6 +36,7 @@ class Milestone < ApplicationRecord
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
+ validate :uniqueness_of_title, if: :title_changed?
state_machine :state, initial: :active do
event :close do
@@ -172,4 +173,16 @@ class Milestone < ApplicationRecord
def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end
+
+ # milestone titles must be unique across project and group milestones
+ def uniqueness_of_title
+ if project
+ relation = self.class.for_projects_and_groups([project_id], [project.group&.id])
+ elsif group
+ relation = self.class.for_projects_and_groups(group.projects.select(:id), [group.id])
+ end
+
+ title_exists = relation.find_by_title(title)
+ errors.add(:title, _("already being used for another group or project %{timebox_name}.") % { timebox_name: timebox_name }) if title_exists
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 8f03c6145cb..90e06e44165 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -271,14 +271,9 @@ class Namespace < ApplicationRecord
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
- namespace = user? ? self : self_and_descendants
- Project.where(namespace: namespace)
- end
+ namespace = user? ? self : self_and_descendant_ids
- # Includes pipelines from this namespace and pipelines from all subgroups
- # that belongs to this namespace
- def all_pipelines
- Ci::Pipeline.where(project: all_projects)
+ Project.where(namespace: namespace)
end
def has_parent?
@@ -442,12 +437,6 @@ class Namespace < ApplicationRecord
end
def all_projects_with_pages
- if all_projects.pages_metadata_not_migrated.exists?
- Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation(
- all_projects.pages_metadata_not_migrated
- )
- end
-
all_projects.with_pages_deployed
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 75b8169b58e..600abc33471 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -14,7 +14,8 @@ class NamespaceSetting < ApplicationRecord
before_validation :normalize_default_branch_name
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
- :lock_delayed_project_removal, :resource_access_token_creation_allowed].freeze
+ :lock_delayed_project_removal, :resource_access_token_creation_allowed,
+ :prevent_sharing_groups_outside_hierarchy].freeze
self.primary_key = :namespace_id
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index a1711bc5ee0..d0281f4d974 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -46,6 +46,12 @@ module Namespaces
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
+ # When filtering namespaces by the traversal_ids column to compile a
+ # list of namespace IDs, it's much faster to reference the ID in
+ # traversal_ids than the primary key ID column.
+ # WARNING This scope must be used behind a linear query feature flag
+ # such as `use_traversal_ids`.
+ scope :as_ids, -> { select('traversal_ids[array_length(traversal_ids, 1)] AS id') }
end
def sync_traversal_ids?
@@ -58,12 +64,30 @@ module Namespaces
traversal_ids.present?
end
+ def root_ancestor
+ return super if parent.nil?
+ return super unless persisted?
+
+ return super if traversal_ids.blank?
+ return super unless Feature.enabled?(:use_traversal_ids_for_root_ancestor, default_enabled: :yaml)
+
+ strong_memoize(:root_ancestor) do
+ Namespace.find_by(id: traversal_ids.first)
+ end
+ end
+
def self_and_descendants
return super unless use_traversal_ids?
lineage(top: self)
end
+ def self_and_descendant_ids
+ return super unless use_traversal_ids?
+
+ self_and_descendants.as_ids
+ end
+
def descendants
return super unless use_traversal_ids?
@@ -88,7 +112,8 @@ module Namespaces
# Clear any previously memoized root_ancestor as our ancestors have changed.
clear_memoization(:root_ancestor)
- Namespace::TraversalHierarchy.for_namespace(root_ancestor).sync_traversal_ids!
+ # We cannot rely on Namespaces::Traversal::Linear#root_ancestor because it might be stale
+ Namespace::TraversalHierarchy.for_namespace(recursive_root_ancestor).sync_traversal_ids!
end
# Lock the root of the hierarchy we just left, and lock the root of the hierarchy
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
index 409438f53d2..5a1a9d24117 100644
--- a/app/models/namespaces/traversal/recursive.rb
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -16,6 +16,7 @@ module Namespaces
parent.root_ancestor
end
end
+ alias_method :recursive_root_ancestor, :root_ancestor
# Returns all ancestors, self, and descendants of the current namespace.
def self_and_hierarchy
@@ -61,6 +62,11 @@ module Namespaces
end
alias_method :recursive_self_and_descendants, :self_and_descendants
+ def self_and_descendant_ids
+ self_and_descendants.select(:id)
+ end
+ alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
+
def object_hierarchy(ancestors_base)
Gitlab::ObjectHierarchy.new(ancestors_base, options: { use_distinct: Feature.enabled?(:use_distinct_in_object_hierarchy, self) })
end
diff --git a/app/models/note.rb b/app/models/note.rb
index ae4a8859d4d..d1a59394ba1 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -96,7 +96,9 @@ class Note < ApplicationRecord
validate :does_not_exceed_notes_limit?, on: :create, unless: [:system?, :importing?]
- # @deprecated attachments are handler by the MarkdownUploader
+ # @deprecated attachments are handled by the Upload model.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/20830
mount_uploader :attachment, AttachmentUploader
# Scopes
@@ -274,6 +276,10 @@ class Note < ApplicationRecord
noteable_type == 'AlertManagement::Alert'
end
+ def for_vulnerability?
+ noteable_type == "Vulnerability"
+ end
+
def for_project_snippet?
noteable.is_a?(ProjectSnippet)
end
@@ -409,6 +415,8 @@ class Note < ApplicationRecord
'snippet'
elsif for_alert_mangement_alert?
'alert_management_alert'
+ elsif for_vulnerability?
+ 'security_resource'
else
noteable_type.demodulize.underscore
end
diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb
index be76c3dbf9d..9185547d7cd 100644
--- a/app/models/onboarding_progress.rb
+++ b/app/models/onboarding_progress.rb
@@ -85,6 +85,10 @@ class OnboardingProgress < ApplicationRecord
end
end
+ def number_of_completed_actions
+ attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size
+ end
+
private
def namespace_is_root_namespace
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 537543a7ff0..8b052f80395 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -49,6 +49,8 @@ module Operations
scope :enabled, -> { where(active: true) }
scope :disabled, -> { where(active: false) }
+ scope :new_version_only, -> { where(version: :new_version_flag)}
+
enum version: {
legacy_flag: 1,
new_version_flag: 2
diff --git a/app/models/packages/debian/group_distribution_key.rb b/app/models/packages/debian/group_distribution_key.rb
new file mode 100644
index 00000000000..a60ddca32e2
--- /dev/null
+++ b/app/models/packages/debian/group_distribution_key.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::GroupDistributionKey < ApplicationRecord
+ def self.container_type
+ :group
+ end
+
+ include Packages::Debian::DistributionKey
+end
diff --git a/app/models/packages/debian/project_distribution_key.rb b/app/models/packages/debian/project_distribution_key.rb
new file mode 100644
index 00000000000..69cf2791b02
--- /dev/null
+++ b/app/models/packages/debian/project_distribution_key.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Packages::Debian::ProjectDistributionKey < ApplicationRecord
+ def self.container_type
+ :project
+ end
+
+ include Packages::Debian::DistributionKey
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 36edf646658..7b0bb72940e 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -8,6 +8,23 @@ class Packages::Package < ApplicationRecord
DISPLAYABLE_STATUSES = [:default, :error].freeze
INSTALLABLE_STATUSES = [:default].freeze
+ enum package_type: {
+ maven: 1,
+ npm: 2,
+ conan: 3,
+ nuget: 4,
+ pypi: 5,
+ composer: 6,
+ generic: 7,
+ golang: 8,
+ debian: 9,
+ rubygems: 10,
+ helm: 11,
+ terraform_module: 12
+ }
+
+ enum status: { default: 0, hidden: 1, processing: 2, error: 3 }
+
belongs_to :project
belongs_to :creator, class_name: 'User'
@@ -59,7 +76,7 @@ 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.prefixed_semver_regex }, if: :helm?
+ validates :version, format: { with: Gitlab::Regex.helm_version_regex }, if: :helm?
validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? || terraform_module? }
validates :version,
@@ -72,12 +89,6 @@ class Packages::Package < ApplicationRecord
if: :debian_package?
validate :forbidden_debian_changes, if: :debian?
- enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5,
- composer: 6, generic: 7, golang: 8, debian: 9,
- 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)) }
@@ -133,14 +144,24 @@ class Packages::Package < ApplicationRecord
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') }
- scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') }
scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') }
+ scope :order_project_path, -> do
+ keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :asc)
+
+ joins(:project).reorder(keyset_order)
+ end
+
+ scope :order_project_path_desc, -> do
+ keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :desc)
+
+ joins(:project).reorder(keyset_order)
+ end
+
after_commit :update_composer_cache, on: :destroy, if: -> { composer? }
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)
+ if use_cte
# This is an optimization fence which assumes that looking up the Metadatum record by path (globally)
# and then filter down the packages (by project or by group and subgroups) will be cheaper than
# looking up all packages within a project or group and filter them by path.
@@ -196,6 +217,32 @@ class Packages::Package < ApplicationRecord
end
end
+ def self.keyset_pagination_order(join_class:, column_name:, direction: :asc)
+ join_table = join_class.table_name
+ asc_order_expression = Gitlab::Database.nulls_last_order("#{join_table}.#{column_name}", :asc)
+ desc_order_expression = Gitlab::Database.nulls_first_order("#{join_table}.#{column_name}", :desc)
+ order_direction = direction == :asc ? asc_order_expression : desc_order_expression
+ reverse_order_direction = direction == :asc ? desc_order_expression : asc_order_expression
+ arel_order_classes = ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::AREL_ORDER_CLASSES.invert
+
+ ::Gitlab::Pagination::Keyset::Order.build([
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: "#{join_table}_#{column_name}",
+ column_expression: join_class.arel_table[column_name],
+ order_expression: order_direction,
+ reversed_order_expression: reverse_order_direction,
+ order_direction: direction,
+ distinct: false,
+ add_to_projections: true
+ ),
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_order_classes[direction].new(Packages::Package.arel_table[:id]),
+ add_to_projections: true
+ )
+ ])
+ end
+
def versions
project.packages
.including_build_info
@@ -222,6 +269,10 @@ class Packages::Package < ApplicationRecord
tags.pluck(:name)
end
+ def infrastructure_package?
+ terraform_module?
+ end
+
def debian_incoming?
debian? && version.nil?
end
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 3d8641ca2fa..3ef30c035e8 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -33,11 +33,18 @@ class Packages::PackageFile < ApplicationRecord
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
+ scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) }
scope :for_rubygem_with_file_name, ->(project, file_name) do
joins(:package).merge(project.packages.rubygems).with_file_name(file_name)
end
+ scope :for_helm_with_channel, ->(project, channel) do
+ joins(:package).merge(project.packages.helm.installable)
+ .joins(:helm_file_metadatum)
+ .where(packages_helm_file_metadata: { channel: channel })
+ end
+
scope :with_conan_file_type, ->(file_type) do
joins(:conan_file_metadatum)
.where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] })
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 17131cd736d..e7d455085c0 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -26,7 +26,18 @@ module Pages
end
def source
- zip_source || legacy_source
+ return unless deployment&.file
+
+ global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
+
+ {
+ type: 'zip',
+ path: deployment.file.url_or_file_path(expire_at: 1.day.from_now),
+ global_id: global_id,
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
+ }
end
def prefix
@@ -46,32 +57,5 @@ module Pages
project.pages_metadatum.pages_deployment
end
end
-
- def zip_source
- return unless deployment&.file
-
- global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
-
- {
- type: 'zip',
- path: deployment.file.url_or_file_path(expire_at: 1.day.from_now),
- global_id: global_id,
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- }
- 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
- return unless ::Settings.pages.local_store.enabled
-
- {
- type: 'file',
- path: File.join(project.full_path, 'public/')
- }
- end
end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 4668fc265a0..c932d0bf800 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -50,6 +50,8 @@ class PagesDomain < ApplicationRecord
after_update :update_daemon, if: :saved_change_to_pages_config?
after_destroy :update_daemon
+ scope :for_project, ->(project) { where(project: project) }
+
scope :enabled, -> { where('enabled_until >= ?', Time.current ) }
scope :needs_verification, -> do
verified_at = arel_table[:verified_at]
@@ -225,16 +227,6 @@ class PagesDomain < ApplicationRecord
def pages_deployed?
return false unless project
- # TODO: remove once `pages_metadatum` is migrated
- # https://gitlab.com/gitlab-org/gitlab/issues/33106
- unless project.pages_metadatum
- Gitlab::BackgroundMigration::MigratePagesMetadata
- .new
- .perform_on_relation(Project.where(id: project_id))
-
- project.reset
- end
-
project.pages_metadatum&.deployed?
end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index c96786423e5..77b42c34ad9 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -26,8 +26,8 @@ module Postgresql
"(pg_current_wal_insert_lsn(), restart_lsn)::bigint"
# We force the use of a transaction here so the query always goes to the
- # primary, even when using the EE DB load balancer.
- sizes = transaction { pluck(lag_function) }
+ # primary, even when using the DB load balancer.
+ sizes = transaction { pluck(Arel.sql(lag_function)) }
too_great = sizes.compact.count { |size| size >= max }
# If too many replicas are falling behind too much, the availability of a
diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
index 671091480ee..c0ed56057ae 100644
--- a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Preloaders
- # This class preloads the max access level for the user within the given projects and
+ # This class preloads the max access level (role) for the user within the given projects and
# stores the values in requests store via the ProjectTeam class.
class UserMaxAccessLevelInProjectsPreloader
def initialize(projects, user)
diff --git a/app/models/project.rb b/app/models/project.rb
index 9d572b7e2f8..735dc185575 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -63,8 +63,6 @@ class Project < ApplicationRecord
VALID_MIRROR_PORTS = [22, 80, 443].freeze
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
- ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
-
SORTING_PREFERENCE_FIELD = :projects_sort
MAX_BUILD_TIMEOUT = 1.month
@@ -129,40 +127,6 @@ class Project < ApplicationRecord
after_create :check_repository_absence!
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
@@ -182,44 +146,51 @@ class Project < ApplicationRecord
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards
+ def self.integration_association_name(name)
+ if ::Integration.renamed?(name)
+ "#{name}_integration"
+ else
+ "#{name}_service"
+ end
+ end
+
# 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 :asana_integration, class_name: 'Integrations::Asana'
+ has_one :assembla_integration, class_name: 'Integrations::Assembla'
+ has_one :bamboo_integration, class_name: 'Integrations::Bamboo'
+ has_one :bugzilla_integration, class_name: 'Integrations::Bugzilla'
+ has_one :buildkite_integration, class_name: 'Integrations::Buildkite'
+ has_one :campfire_integration, class_name: 'Integrations::Campfire'
+ has_one :confluence_integration, class_name: 'Integrations::Confluence'
+ has_one :custom_issue_tracker_integration, class_name: 'Integrations::CustomIssueTracker'
+ has_one :datadog_integration, class_name: 'Integrations::Datadog'
+ has_one :discord_integration, class_name: 'Integrations::Discord'
+ has_one :drone_ci_integration, class_name: 'Integrations::DroneCi'
has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
- has_one :discord_service
- has_one :drone_ci_service
- has_one :ewm_service
- has_one :pipelines_email_service
- has_one :irker_service
- has_one :pivotaltracker_service
- has_one :flowdock_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 :teamcity_service
- has_one :pushover_service
- has_one :jenkins_service
- has_one :jira_service
- has_one :redmine_service
- has_one :youtrack_service
- has_one :custom_issue_tracker_service
- has_one :bugzilla_service
- has_one :external_wiki_service
+ has_one :ewm_service, class_name: 'Integrations::Ewm'
+ has_one :external_wiki_service, class_name: 'Integrations::ExternalWiki'
+ has_one :flowdock_service, class_name: 'Integrations::Flowdock'
+ has_one :hangouts_chat_service, class_name: 'Integrations::HangoutsChat'
+ has_one :irker_service, class_name: 'Integrations::Irker'
+ has_one :jenkins_service, class_name: 'Integrations::Jenkins'
+ has_one :jira_service, class_name: 'Integrations::Jira'
+ has_one :mattermost_service, class_name: 'Integrations::Mattermost'
+ has_one :mattermost_slash_commands_service, class_name: 'Integrations::MattermostSlashCommands'
+ has_one :microsoft_teams_service, class_name: 'Integrations::MicrosoftTeams'
+ has_one :mock_ci_service, class_name: 'Integrations::MockCi'
+ has_one :packagist_service, class_name: 'Integrations::Packagist'
+ has_one :pipelines_email_service, class_name: 'Integrations::PipelinesEmail'
+ has_one :pivotaltracker_service, class_name: 'Integrations::Pivotaltracker'
+ has_one :pushover_service, class_name: 'Integrations::Pushover'
+ has_one :redmine_service, class_name: 'Integrations::Redmine'
+ has_one :slack_service, class_name: 'Integrations::Slack'
+ has_one :slack_slash_commands_service, class_name: 'Integrations::SlackSlashCommands'
+ has_one :teamcity_service, class_name: 'Integrations::Teamcity'
+ has_one :unify_circuit_service, class_name: 'Integrations::UnifyCircuit'
+ has_one :webex_teams_service, class_name: 'Integrations::WebexTeams'
+ has_one :youtrack_service, class_name: 'Integrations::Youtrack'
has_one :prometheus_service, inverse_of: :project
- has_one :mock_ci_service
- has_one :mock_deployment_service
has_one :mock_monitoring_service
- has_one :microsoft_teams_service
- has_one :packagist_service
- has_one :hangouts_chat_service
- has_one :unify_circuit_service
- has_one :webex_teams_service
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
@@ -261,7 +232,15 @@ class Project < ApplicationRecord
has_many :events
has_many :milestones
has_many :iterations
- has_many :notes
+
+ # Projects with a very large number of notes may time out destroying them
+ # through the foreign key. Additionally, the deprecated attachment uploader
+ # for notes requires us to use dependent: :destroy to avoid orphaning uploaded
+ # files.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/207222
+ has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches
@@ -287,7 +266,7 @@ class Project < ApplicationRecord
has_many :users_star_projects
has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
- has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :lfs_objects_projects
has_many :lfs_objects, -> { distinct }, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
@@ -439,6 +418,7 @@ class Project < ApplicationRecord
delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
prefix: :import, to: :import_state, allow_nil: true
delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
+ delegate :squash_option, to: :project_setting
delegate :no_import?, to: :import_state, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -449,11 +429,12 @@ class Project < ApplicationRecord
delegate :last_pipeline, to: :commit, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
- delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
- delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci
- delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, :keep_latest_artifacts_available?, to: :ci_cd_settings
+ delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
+ delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci, allow_nil: true
+ delegate :job_token_scope_enabled, :job_token_scope_enabled=, :job_token_scope_enabled?, to: :ci_cd_settings, prefix: :ci
+ delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, :keep_latest_artifacts_available?, to: :ci_cd_settings, allow_nil: true
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?,
- to: :ci_cd_settings
+ to: :ci_cd_settings, allow_nil: true
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
:allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?,
@@ -561,7 +542,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(:integrations).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
+ scope :with_active_jira_services, -> { joins(:integrations).merge(::Integrations::Jira.active) }
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) }
@@ -637,6 +618,12 @@ class Project < ApplicationRecord
scope :with_tracing_enabled, -> { joins(:tracing_setting) }
scope :with_enabled_error_tracking, -> { joins(:error_tracking_setting).where(project_error_tracking_settings: { enabled: true }) }
+ scope :with_service_desk_key, -> (key) do
+ # project_key is not indexed for now
+ # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24063#note_282435524 for details
+ joins(:service_desk_setting).where('service_desk_settings.project_key' => key)
+ end
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@@ -652,7 +639,7 @@ class Project < ApplicationRecord
mount_uploader :bfg_object_map, AttachmentUploader
def self.with_api_entity_associations
- preload(:project_feature, :route, :tags, :group, :timelogs, namespace: [:route, :owner])
+ preload(:project_feature, :route, :topics, :group, :timelogs, namespace: [:route, :owner])
end
def self.with_web_entity_associations
@@ -838,12 +825,6 @@ class Project < ApplicationRecord
from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
end
-
- def find_by_service_desk_project_key(key)
- # project_key is not indexed for now
- # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24063#note_282435524 for details
- joins(:service_desk_setting).find_by('service_desk_settings.project_key' => key)
- end
end
def initialize(attributes = nil)
@@ -921,6 +902,10 @@ class Project < ApplicationRecord
alias_method :ancestors, :ancestors_upto
+ def ancestors_upto_ids(...)
+ ancestors_upto(...).pluck(:id)
+ end
+
def emails_disabled?
strong_memoize(:emails_disabled) do
# disabling in the namespace overrides the project setting
@@ -1407,9 +1392,9 @@ class Project < ApplicationRecord
end
def disabled_services
- return %w[datadog hipchat] unless Feature.enabled?(:datadog_ci_integration, self)
+ return %w[datadog] unless Feature.enabled?(:datadog_ci_integration, self)
- %w[hipchat]
+ []
end
def find_or_initialize_service(name)
@@ -1421,7 +1406,8 @@ class Project < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
- params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
+ # TODO: remove_on_close exception can be removed after the column is dropped from all envs
+ params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type', 'remove_on_close')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
@@ -1735,7 +1721,11 @@ class Project < ApplicationRecord
end
def shared_runners
- @shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
+ @shared_runners ||= shared_runners_enabled? ? Ci::Runner.instance_type : Ci::Runner.none
+ end
+
+ def available_shared_runners
+ @available_shared_runners ||= shared_runners_available? ? shared_runners : Ci::Runner.none
end
def group_runners
@@ -1746,17 +1736,16 @@ class Project < ApplicationRecord
Ci::Runner.from_union([runners, group_runners, shared_runners])
end
+ def all_available_runners
+ Ci::Runner.from_union([runners, group_runners, available_shared_runners])
+ end
+
def active_runners
strong_memoize(:active_runners) do
- all_runners.active
+ all_available_runners.active
end
end
- # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/326989
- def any_active_runners?(&block)
- active_runners_with_tags.any?(&block)
- end
-
def any_online_runners?(&block)
online_runners_with_tags.any?(&block)
end
@@ -1772,7 +1761,7 @@ class Project < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
- def open_merge_requests_count
+ def open_merge_requests_count(_current_user = nil)
Projects::OpenMergeRequestsCountService.new(self).count
end
# rubocop: enable CodeReuse/ServiceClass
@@ -2006,7 +1995,11 @@ class Project < ApplicationRecord
end
def export_file_exists?
- export_file&.file
+ import_export_upload&.export_file_exists?
+ end
+
+ def export_archive_exists?
+ import_export_upload&.export_archive_exists?
end
def export_file
@@ -2046,7 +2039,6 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
.append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase)
.append(key: 'CI_DEFAULT_BRANCH', value: default_branch)
- .append(key: 'CI_PROJECT_CONFIG_PATH', value: ci_config_path_or_default)
.append(key: 'CI_CONFIG_PATH', value: ci_config_path_or_default)
end
@@ -2377,7 +2369,7 @@ class Project < ApplicationRecord
end
def mark_primary_write_location
- # Overriden in EE
+ ::Gitlab::Database::LoadBalancing::Sticking.mark_primary_write_location(:project, self.id)
end
def toggle_ci_cd_settings!(settings_attribute)
@@ -2454,7 +2446,7 @@ class Project < ApplicationRecord
end
def access_request_approvers_to_be_notified
- members.maintainers.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ members.maintainers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def pages_lookup_path(trim_prefix: nil, domain: nil)
@@ -2562,6 +2554,17 @@ class Project < ApplicationRecord
end
end
+ # for projects that are part of user namespace, return project.
+ def self_or_root_group_ids
+ if group
+ root_group = root_namespace
+ else
+ project = self
+ end
+
+ [project&.id, root_group&.id]
+ end
+
def package_already_taken?(package_name)
namespace.root_ancestor.all_projects
.joins(:packages)
@@ -2604,10 +2607,6 @@ class Project < ApplicationRecord
Projects::GitGarbageCollectWorker
end
- def inherited_issuable_templates_enabled?
- Feature.enabled?(:inherited_issuable_templates, self, default_enabled: :yaml)
- end
-
def activity_path
Gitlab::Routing.url_helpers.activity_project_path(self)
end
@@ -2618,6 +2617,19 @@ class Project < ApplicationRecord
ProjectStatistics.increment_statistic(self, statistic, delta)
end
+ def merge_requests_author_approval
+ !!read_attribute(:merge_requests_author_approval)
+ end
+
+ def container_registry_enabled
+ if Feature.enabled?(:read_container_registry_access_level, self.namespace, default_enabled: :yaml)
+ project_feature.container_registry_enabled?
+ else
+ read_attribute(:container_registry_enabled)
+ end
+ end
+ alias_method :container_registry_enabled?, :container_registry_enabled
+
private
def set_container_registry_access_level
@@ -2647,7 +2659,7 @@ class Project < ApplicationRecord
end
def build_service(name)
- Integration.service_name_to_model(name).new(project_id: id)
+ Integration.integration_name_to_model(name).new(project_id: id)
end
def services_templates
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 1fed166e4d0..64e768007ee 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -29,6 +29,15 @@ class ProjectAuthorization < ApplicationRecord
EOF
end
end
+
+ # This method overrides its ActiveRecord's version in order to work correctly
+ # with composite primary keys and fix the tests for Rails 6.1
+ #
+ # Consider using BulkInsertSafe module instead since we plan to refactor it in
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331264
+ def self.insert_all(attributes)
+ super(attributes, unique_by: connection.schema_cache.primary_keys(table_name))
+ end
end
ProjectAuthorization.prepend_mod_with('ProjectAuthorization')
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index c0c2ea42d46..b025326c6f8 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -16,6 +16,7 @@ class ProjectCiCdSetting < ApplicationRecord
allow_nil: true
default_value_for :forward_deployment_enabled, true
+ default_value_for :job_token_scope_enabled, true
def forward_deployment_enabled?
super && ::Feature.enabled?(:forward_deployment_enabled, project, default_enabled: true)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index eb4ad327438..f6e889396c6 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -24,7 +24,11 @@ class ProjectFeature < ApplicationRecord
set_available_features(FEATURES)
- PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = {
+ merge_requests: Gitlab::Access::REPORTER,
+ metrics_dashboard: Gitlab::Access::REPORTER,
+ container_registry: Gitlab::Access::REPORTER
+ }.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
class << self
@@ -92,7 +96,7 @@ class ProjectFeature < ApplicationRecord
def set_container_registry_access_level
self.container_registry_access_level =
- if project&.container_registry_enabled
+ if project&.read_attribute(:container_registry_enabled)
ENABLED
else
DISABLED
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index d993db860c3..dba81a6cb60 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -20,14 +20,16 @@ class ProjectFeatureUsage < ApplicationRecord
end
def log_jira_dvcs_integration_usage(cloud: true)
- integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
+ ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
+ integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
- # The feature usage is used only once later to query the feature usage in a
- # long date range. Therefore, we just need to update the timestamp once per
- # day
- return if persisted? && updated_today?(integration_field)
+ # The feature usage is used only once later to query the feature usage in a
+ # long date range. Therefore, we just need to update the timestamp once per
+ # day
+ break if persisted? && updated_today?(integration_field)
- persist_jira_dvcs_usage(integration_field)
+ persist_jira_dvcs_usage(integration_field)
+ end
end
private
diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb
deleted file mode 100644
index e54489ddb88..00000000000
--- a/app/models/project_repository_storage_move.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-# This is a compatibility class to avoid calling a non-existent
-# class from sidekiq during deployment.
-#
-# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
-# we cannot remove this class entirely because there can be jobs
-# referencing it.
-#
-# We can get rid of this class in 14.0
-# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
-class ProjectRepositoryStorageMove < Projects::RepositoryStorageMove
-end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
deleted file mode 100644
index d1c56d2a4d5..00000000000
--- a/app/models/project_services/bugzilla_service.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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
- 'Bugzilla'
- end
-
- def description
- 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
- 'bugzilla'
- end
-end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
deleted file mode 100644
index f2ea5066e37..00000000000
--- a/app/models/project_services/buildkite_service.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-# frozen_string_literal: true
-
-require "addressable/uri"
-
-class BuildkiteService < CiService
- include ReactiveService
-
- ENDPOINT = "https://buildkite.com"
-
- prop_accessor :project_url, :token
-
- validates :project_url, presence: true, public_url: true, if: :activated?
- validates :token, presence: true, if: :activated?
-
- after_save :compose_service_hook, if: :activated?
-
- def self.supported_events
- %w(push merge_request tag_push)
- end
-
- # This is a stub method to work with deprecated API response
- # TODO: remove enable_ssl_verification after 14.0
- # https://gitlab.com/gitlab-org/gitlab/-/issues/222808
- def enable_ssl_verification
- true
- end
-
- # Since SSL verification will always be enabled for Buildkite,
- # we no longer needs to store the boolean.
- # This is a stub method to work with deprecated API param.
- # TODO: remove enable_ssl_verification after 14.0
- # https://gitlab.com/gitlab-org/gitlab/-/issues/222808
- def enable_ssl_verification=(_value)
- self.properties.delete('enable_ssl_verification') # Remove unused key
- end
-
- def webhook_url
- "#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
- end
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.url = webhook_url
- hook.enable_ssl_verification = true
- hook.save
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- service_hook.execute(data)
- end
-
- def commit_status(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
- end
-
- def commit_status_path(sha)
- "#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
- end
-
- def build_page(sha, ref)
- "#{project_url}/builds?commit=#{sha}"
- end
-
- def title
- 'Buildkite'
- end
-
- def description
- 'Run CI/CD pipelines with Buildkite.'
- end
-
- def self.to_param
- 'buildkite'
- end
-
- def fields
- [
- { type: 'text',
- name: 'token',
- title: 'Integration Token',
- help: 'This token will be provided when you create a Buildkite pipeline with a GitLab repository',
- required: true },
-
- { type: 'text',
- name: 'project_url',
- title: 'Pipeline URL',
- placeholder: "#{ENDPOINT}/acme-inc/test-pipeline",
- required: true }
- ]
- end
-
- def calculate_reactive_cache(sha, ref)
- response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
-
- status =
- if response&.code == 200 && response['status']
- response['status']
- else
- :error
- end
-
- { commit_status: status }
- end
-
- private
-
- def webhook_token
- token_parts.first
- end
-
- def status_token
- token_parts.second
- end
-
- def token_parts
- if token.present?
- token.split(':')
- else
- []
- end
- end
-
- def buildkite_endpoint(subdomain = nil)
- if subdomain.present?
- uri = Addressable::URI.parse(ENDPOINT)
- new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}"
-
- if uri.port.present?
- "#{new_endpoint}:#{uri.port}"
- else
- new_endpoint
- end
- else
- ENDPOINT
- end
- end
-
- def request_options
- { verify: false, extra_log_info: { project_id: project_id } }
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
deleted file mode 100644
index 2f841bf903e..00000000000
--- a/app/models/project_services/chat_notification_service.rb
+++ /dev/null
@@ -1,252 +0,0 @@
-# frozen_string_literal: true
-
-# Base class for Chat notifications services
-# This class is not meant to be used directly, but only to inherit from.
-class ChatNotificationService < Integration
- include ChatMessage
- include NotificationBranchSelection
-
- SUPPORTED_EVENTS = %w[
- push issue confidential_issue merge_request note confidential_note
- tag_push pipeline wiki_page deployment
- ].freeze
-
- SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
-
- 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, :labels_to_be_notified_behavior
-
- # Custom serialized properties initialization
- prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
-
- 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
- # `branches_to_be_notified`. Instead of doing a background migration, we
- # opted to set a value for the new property based on the old one, if
- # users hasn't specified one already. When users edit the service and
- # selects a value for this new property, it will override everything.
-
- self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
- end
- end
-
- def confidential_issue_channel
- properties['confidential_issue_channel'].presence || properties['issue_channel']
- end
-
- def confidential_note_channel
- properties['confidential_note_channel'].presence || properties['note_channel']
- end
-
- def self.supported_events
- SUPPORTED_EVENTS
- end
-
- def fields
- default_fields + build_event_channels
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}", required: true }.freeze,
- { 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: '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
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- return unless notify_label?(data)
-
- return unless webhook.present?
-
- object_kind = data[:object_kind]
-
- data = custom_data(data)
-
- # WebHook events often have an 'update' event that follows a 'open' or
- # 'close' action. Ignore update events for now to prevent duplicate
- # messages from arriving.
-
- message = get_message(object_kind, data)
-
- return false unless message
-
- event_type = data[:event_type] || object_kind
-
- channel_names = get_channel_field(event_type).presence || channel.presence
- channels = channel_names&.split(',')&.map(&:strip)
-
- opts = {}
- opts[:channel] = channels if channels.present?
- opts[:username] = username if username
-
- if notify(message, opts)
- log_usage(event_type, user_id_from_hook_data(data))
- return true
- end
-
- false
- end
-
- def event_channel_names
- supported_events.map { |event| event_channel_name(event) }
- end
-
- def event_field(event)
- fields.find { |field| field[:name] == event_channel_name(event) }
- end
-
- def global_fields
- fields.reject { |field| field[:name].end_with?('channel') }
- end
-
- def default_channel_placeholder
- raise NotImplementedError
- end
-
- private
-
- def log_usage(_, _)
- # Implement in child class
- end
-
- def labels_to_be_notified_list
- return [] if labels_to_be_notified.nil?
-
- labels_to_be_notified.delete('~').split(',').map(&:strip)
- end
-
- def notify_label?(data)
- return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
-
- labels = data.dig(:issue, :labels) || data.dig(:merge_request, :labels)
-
- return false if labels.nil?
-
- 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)
- data.dig(:user, :id) || data[:user_id]
- end
-
- # every notifier must implement this independently
- def notify(message, opts)
- raise NotImplementedError
- end
-
- def custom_data(data)
- data.merge(project_url: project_url, project_name: project_name)
- end
-
- def get_message(object_kind, data)
- case object_kind
- when "push", "tag_push"
- Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
- when "issue"
- Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
- when "merge_request"
- Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
- when "note"
- Integrations::ChatMessage::NoteMessage.new(data)
- when "pipeline"
- Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
- when "wiki_page"
- Integrations::ChatMessage::WikiPageMessage.new(data)
- when "deployment"
- Integrations::ChatMessage::DeploymentMessage.new(data)
- end
- end
-
- def get_channel_field(event)
- field_name = event_channel_name(event)
- self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def build_event_channels
- supported_events.reduce([]) do |channels, event|
- channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
- end
- end
-
- def event_channel_name(event)
- EVENT_CHANNEL[event]
- end
-
- def project_name
- project.full_name
- end
-
- def project_url
- project.web_url
- end
-
- def update?(data)
- data[:object_attributes][:action] == 'update'
- end
-
- def should_pipeline_be_notified?(data)
- notify_for_ref?(data) && notify_for_pipeline?(data)
- end
-
- def notify_for_ref?(data)
- return true if data[:object_kind] == 'tag_push'
- return true if data.dig(:object_attributes, :tag)
-
- notify_for_branch?(data)
- end
-
- def notify_for_pipeline?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
- end
-end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
deleted file mode 100644
index 0733da761d5..00000000000
--- a/app/models/project_services/ci_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-# Base class for CI services
-# List methods you need to implement to get your CI service
-# working with GitLab merge requests
-class CiService < Integration
- default_value_for :category, 'ci'
-
- def valid_token?(token)
- self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
- end
-
- def self.supported_events
- %w(push)
- end
-
- # Return complete url to build page
- #
- # Ex.
- # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
- #
- def build_page(sha, ref)
- # implement inside child
- end
-
- # Return string with build status or :error symbol
- #
- # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
- #
- #
- # Ex.
- # @service.commit_status('13be4ac', 'master')
- # # => 'success'
- #
- # @service.commit_status('2abe4ac', 'dev')
- # # => 'running'
- #
- #
- def commit_status(sha, ref)
- # implement inside child
- end
-end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
deleted file mode 100644
index 6f99d104904..00000000000
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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
- s_('IssueTracker|Custom issue tracker')
- end
-
- def description
- s_("IssueTracker|Use a custom issue tracker 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/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 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
deleted file mode 100644
index ca4dc0375fb..00000000000
--- a/app/models/project_services/data_fields.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-module DataFields
- extend ActiveSupport::Concern
-
- class_methods do
- # Provide convenient accessor methods for data fields.
- # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
- def data_field(*args)
- args.each do |arg|
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
- unless method_defined?(arg)
- def #{arg}
- data_fields.send('#{arg}') || (properties && properties['#{arg}'])
- end
- end
-
- def #{arg}=(value)
- @old_data_fields ||= {}
- @old_data_fields['#{arg}'] ||= #{arg} # set only on the first assignment, IOW we remember the original value only
- data_fields.send('#{arg}=', value)
- end
-
- def #{arg}_touched?
- @old_data_fields ||= {}
- @old_data_fields.has_key?('#{arg}')
- end
-
- def #{arg}_changed?
- #{arg}_touched? && @old_data_fields['#{arg}'] != #{arg}
- end
-
- def #{arg}_was
- return unless #{arg}_touched?
- return if data_fields.persisted? # arg_was does not work for attr_encrypted
-
- legacy_properties_data['#{arg}']
- end
- RUBY
- end
- end
- end
-
- included do
- 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
- end
-
- def data_fields_present?
- data_fields.present?
- rescue NotImplementedError
- false
- end
- end
-end
diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb
deleted file mode 100644
index d7adf63fde4..00000000000
--- a/app/models/project_services/discord_service.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require "discordrb/webhooks"
-
-class DiscordService < ChatNotificationService
- include ActionView::Helpers::UrlHelper
-
- ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
-
- def title
- s_("DiscordService|Discord Notifications")
- end
-
- def description
- s_("DiscordService|Send notifications about project events to a Discord channel.")
- end
-
- def self.to_param
- "discord"
- end
-
- def help
- docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
- s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def event_field(event)
- # No-op.
- end
-
- def default_channel_placeholder
- # No-op.
- end
-
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
- end
-
- def default_fields
- [
- { type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
- { type: "checkbox", name: "notify_only_broken_pipelines" },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
- ]
- end
-
- private
-
- def notify(message, opts)
- client = Discordrb::Webhooks::Client.new(url: webhook)
-
- client.execute do |builder|
- builder.add_embed do |embed|
- embed.author = Discordrb::Webhooks::EmbedAuthor.new(name: message.user_name, icon_url: message.user_avatar)
- embed.description = (message.pretext + "\n" + Array.wrap(message.attachments).join("\n")).gsub(ATTACHMENT_REGEX, " \\k<entry> - \\k<name>\n")
- end
- end
- rescue RestClient::Exception => error
- log_error(error.message)
- false
- end
-
- def custom_data(data)
- super(data).merge(markdown: true)
- end
-end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
deleted file mode 100644
index ab1ba768a8f..00000000000
--- a/app/models/project_services/drone_ci_service.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-# frozen_string_literal: true
-
-class DroneCiService < CiService
- include ReactiveService
- include ServicePushDataValidations
-
- prop_accessor :drone_url, :token
- boolean_accessor :enable_ssl_verification
-
- validates :drone_url, presence: true, public_url: true, if: :activated?
- validates :token, presence: true, if: :activated?
-
- after_save :compose_service_hook, if: :activated?
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- # If using a service template, project may not be available
- hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
- hook.enable_ssl_verification = !!enable_ssl_verification
- hook.save
- end
-
- def execute(data)
- case data[:object_kind]
- when 'push'
- service_hook.execute(data) if push_valid?(data)
- when 'merge_request'
- service_hook.execute(data) if merge_request_valid?(data)
- when 'tag_push'
- service_hook.execute(data) if tag_push_valid?(data)
- end
- end
-
- def allow_target_ci?
- true
- end
-
- def self.supported_events
- %w(push merge_request tag_push)
- end
-
- def commit_status_path(sha, ref)
- Gitlab::Utils.append_path(
- drone_url,
- "gitlab/#{project.full_path}/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}&access_token=#{token}")
- end
-
- def commit_status(sha, ref)
- with_reactive_cache(sha, ref) { |cached| cached[:commit_status] }
- end
-
- def calculate_reactive_cache(sha, ref)
- response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
- verify: enable_ssl_verification,
- extra_log_info: { project_id: project_id })
-
- status =
- if response && response.code == 200 && response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
- else
- response["status"]
- end
- else
- :error
- end
-
- { commit_status: status }
- end
-
- def build_page(sha, ref)
- Gitlab::Utils.append_path(
- drone_url,
- "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
- end
-
- def title
- 'Drone'
- end
-
- def description
- s_('ProjectService|Run CI/CD pipelines with Drone.')
- end
-
- def self.to_param
- 'drone_ci'
- end
-
- def help
- s_('ProjectService|Run CI/CD pipelines with Drone.')
- end
-
- def fields
- [
- { type: 'text', name: 'token', help: s_('ProjectService|Token for the Drone project.'), required: true },
- { type: 'text', name: 'drone_url', title: s_('ProjectService|Drone server URL'), placeholder: 'http://drone.example.com', required: true },
- { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
- ]
- end
-end
diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb
deleted file mode 100644
index 90fcbb10d2b..00000000000
--- a/app/models/project_services/ewm_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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)
- @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
- end
-
- def title
- 'EWM'
- end
-
- def description
- 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
- 'ewm'
- end
-
- def can_test?
- false
- end
-
- def issue_url(iid)
- issues_url.gsub(':id', iid.to_s.split(' ')[-1])
- end
-end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
deleted file mode 100644
index f49b008533d..00000000000
--- a/app/models/project_services/external_wiki_service.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-class ExternalWikiService < Integration
- include ActionView::Helpers::UrlHelper
-
- prop_accessor :external_wiki_url
- validates :external_wiki_url, presence: true, public_url: true, if: :activated?
-
- def title
- s_('ExternalWikiService|External wiki')
- end
-
- def description
- s_('ExternalWikiService|Link to an external wiki from the sidebar.')
- end
-
- def self.to_param
- 'external_wiki'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'external_wiki_url',
- title: s_('ExternalWikiService|External wiki URL'),
- placeholder: s_('ExternalWikiService|https://example.com/xxx/wiki/...'),
- help: 'Enter the URL to the external wiki.',
- required: true
- }
- ]
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer'
-
- s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def execute(_data)
- response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
- response.body if response.code == 200
- rescue StandardError
- nil
- end
-
- def self.supported_events
- %w()
- end
-end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
deleted file mode 100644
index 7aae5af7454..00000000000
--- a/app/models/project_services/flowdock_service.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-class FlowdockService < Integration
- include ActionView::Helpers::UrlHelper
-
- prop_accessor :token
- validates :token, presence: true, if: :activated?
-
- def title
- 'Flowdock'
- end
-
- def description
- 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
- 'flowdock'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: s_('FlowdockService|1b609b52537...'), required: true, help: 'Enter your Flowdock token.' }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- Flowdock::Git.post(
- data[:ref],
- data[:before],
- data[:after],
- token: token,
- repo: project.repository,
- repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
- commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s",
- diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
- )
- end
-end
diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb
deleted file mode 100644
index 6e7708a169f..00000000000
--- a/app/models/project_services/hangouts_chat_service.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'hangouts_chat'
-
-class HangoutsChatService < ChatNotificationService
- include ActionView::Helpers::UrlHelper
-
- def title
- 'Google Chat'
- end
-
- def description
- 'Send notifications from GitLab to a room in Google Chat.'
- end
-
- def self.to_param
- 'hangouts_chat'
- end
-
- def help
- 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)
- end
-
- def default_channel_placeholder
- end
-
- def webhook_placeholder
- 'https://chat.googleapis.com/v1/spaces…'
- end
-
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
- ]
- end
-
- private
-
- def notify(message, opts)
- simple_text = parse_simple_text_message(message)
- HangoutsChat::Sender.new(webhook).simple(simple_text)
- end
-
- def parse_simple_text_message(message)
- header = message.pretext
- return header if message.attachments.empty?
-
- attachment = message.attachments.first
- title = format_attachment_title(attachment)
- body = attachment[:text]
-
- [header, title, body].compact.join("\n")
- end
-
- def format_attachment_title(attachment)
- return attachment[:title] unless attachment[:title_link]
-
- "<#{attachment[:title_link]}|#{attachment[:title]}>"
- end
-end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
deleted file mode 100644
index 71d8e7bfac4..00000000000
--- a/app/models/project_services/hipchat_service.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-# 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 self.supported_events
- []
- end
-
- def execute(data)
- # We removed the hipchat gem due to https://gitlab.com/gitlab-org/gitlab/-/issues/325851#note_537143149
- # HipChat is unusable anyway, so do nothing in this method
- end
-
- private
-
- def prevent_save
- errors.add(:base, _('HipChat endpoint is deprecated and should not be created or modified.'))
-
- # 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
deleted file mode 100644
index 5cca620c659..00000000000
--- a/app/models/project_services/irker_service.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-require 'uri'
-
-class IrkerService < Integration
- prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :recipients, :channels
- boolean_accessor :colorize_messages
- validates :recipients, presence: true, if: :validate_recipients?
-
- before_validation :get_channels
-
- def title
- 'Irker (IRC gateway)'
- end
-
- def description
- 'Send IRC messages.'
- end
-
- def self.to_param
- 'irker'
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- IrkerWorker.perform_async(project_id, channels,
- colorize_messages, data, settings)
- end
-
- def settings
- {
- server_host: server_host.presence || 'localhost',
- server_port: server_port.presence || 6659
- }
- end
-
- def fields
- [
- { type: 'text', name: 'server_host', placeholder: 'localhost',
- help: 'Irker daemon hostname (defaults to localhost)' },
- { type: 'text', name: 'server_port', placeholder: 6659,
- help: 'Irker daemon port (defaults to 6659)' },
- { type: 'text', name: 'default_irc_uri', title: 'Default IRC URI',
- help: 'A default IRC URI to prepend before each recipient (optional)',
- placeholder: 'irc://irc.network.net:6697/' },
- { type: 'textarea', name: 'recipients',
- placeholder: 'Recipients/channels separated by whitespaces', required: true,
- help: 'Recipients have to be specified with a full URI: '\
- 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
- 'you want the channel to be a nickname instead, append ",isnick" to ' \
- 'the channel name; if the channel is protected by a secret password, ' \
- ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
- ' want to use a password, you have to omit the "#" on the channel). If you ' \
- ' specify a default IRC URI to prepend before each recipient, you can just ' \
- ' give a channel name.' },
- { type: 'checkbox', name: 'colorize_messages' }
- ]
- end
-
- def help
- ' NOTE: Irker does NOT have built-in authentication, which makes it' \
- ' vulnerable to spamming IRC channels if it is hosted outside of a ' \
- ' firewall. Please make sure you run the daemon within a secured network ' \
- ' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.'
- end
-
- private
-
- def get_channels
- return true unless activated?
- return true if recipients.nil? || recipients.empty?
-
- map_recipients
-
- errors.add(:recipients, 'are all invalid') if channels.empty?
- true
- end
-
- def map_recipients
- self.channels = recipients.split(/\s+/).map do |recipient|
- format_channel(recipient)
- end
- channels.reject!(&:nil?)
- end
-
- def format_channel(recipient)
- uri = nil
-
- # Try to parse the chan as a full URI
- begin
- uri = consider_uri(URI.parse(recipient))
- rescue URI::InvalidURIError
- end
-
- unless uri.present? && default_irc_uri.nil?
- begin
- new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
- uri = consider_uri(URI.parse(new_recipient))
- rescue StandardError
- log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
- end
- end
-
- uri
- end
-
- def consider_uri(uri)
- return if uri.scheme.nil?
-
- # Authorize both irc://domain.com/#chan and irc://domain.com/chan
- if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil?
- uri.to_s
- end
- end
-end
diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb
deleted file mode 100644
index 414f2c1da4d..00000000000
--- a/app/models/project_services/issue_tracker_data.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class IssueTrackerData < ApplicationRecord
- include Services::DataFields
-
- attr_encrypted :project_url, encryption_options
- attr_encrypted :issues_url, encryption_options
- attr_encrypted :new_issue_url, encryption_options
-end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
deleted file mode 100644
index 099e3c336dd..00000000000
--- a/app/models/project_services/issue_tracker_service.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-# frozen_string_literal: true
-
-class IssueTrackerService < Integration
- validate :one_issue_tracker, if: :activated?, on: :manual_change
-
- # TODO: we can probably just delegate as part of
- # https://gitlab.com/gitlab-org/gitlab/issues/29404
- data_field :project_url, :issues_url, :new_issue_url
-
- default_value_for :category, 'issue_tracker'
-
- before_validation :handle_properties
- before_validation :set_default_data, on: :create
-
- # Pattern used to extract links from comments
- # Override this method on services that uses different patterns
- # This pattern does not support cross-project references
- # The other code assumes that this pattern is a superset of all
- # overridden patterns. See ReferenceRegexes.external_pattern
- def self.reference_pattern(only_long: false)
- if only_long
- /(\b[A-Z][A-Z0-9_]*-)#{Gitlab::Regex.issue}/
- else
- /(\b[A-Z][A-Z0-9_]*-|#{Issue.reference_prefix})#{Gitlab::Regex.issue}/
- end
- end
-
- def handle_properties
- # this has been moved from initialize_properties and should be improved
- # as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
- return unless properties
-
- @legacy_properties_data = properties.dup
- data_values = properties.slice!('title', 'description')
- data_values.reject! { |key| data_fields.changed.include?(key) }
- data_values.slice!(*data_fields.attributes.keys)
- data_fields.assign_attributes(data_values) if data_values.present?
-
- self.properties = {}
- end
-
- def legacy_properties_data
- @legacy_properties_data ||= {}
- end
-
- def supports_data_fields?
- true
- end
-
- def data_fields
- issue_tracker_data || self.build_issue_tracker_data
- end
-
- def default?
- default
- end
-
- def issue_url(iid)
- issues_url.gsub(':id', iid.to_s)
- end
-
- def issue_tracker_path
- project_url
- end
-
- def new_issue_path
- new_issue_url
- end
-
- def issue_path(iid)
- issue_url(iid)
- end
-
- def fields
- [
- { 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
-
- def initialize_properties
- {}
- end
-
- # Initialize with default properties values
- def set_default_data
- return unless issues_tracker.present?
-
- # we don't want to override if we have set something
- return if project_url || issues_url || new_issue_url
-
- data_fields.project_url = issues_tracker['project_url']
- data_fields.issues_url = issues_tracker['issues_url']
- data_fields.new_issue_url = issues_tracker['new_issue_url']
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again."
- result = false
-
- begin
- response = Gitlab::HTTP.head(self.project_url, verify: true)
-
- if response
- message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
- result = true
- end
- rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
- message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
- end
- log_info(message)
- result
- end
-
- def support_close_issue?
- false
- end
-
- def support_cross_reference?
- false
- end
-
- private
-
- def enabled_in_gitlab_config
- Gitlab.config.issues_tracker &&
- Gitlab.config.issues_tracker.values.any? &&
- issues_tracker
- end
-
- def issues_tracker
- Gitlab.config.issues_tracker[to_param]
- end
-
- def one_issue_tracker
- return if template? || instance?
- return if project.blank?
-
- 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_mod_with('IssueTrackerService')
diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb
deleted file mode 100644
index 990a35cd617..00000000000
--- a/app/models/project_services/jenkins_service.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-class JenkinsService < CiService
- include ActionView::Helpers::UrlHelper
-
- prop_accessor :jenkins_url, :project_name, :username, :password
-
- before_update :reset_password
-
- validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
- validates :project_name, presence: true, if: :activated?
- validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
-
- default_value_for :push_events, true
- default_value_for :merge_requests_events, false
- default_value_for :tag_push_events, false
-
- after_save :compose_service_hook, if: :activated?
-
- def reset_password
- # don't reset the password if a new one is provided
- if (jenkins_url_changed? || username.blank?) && !password_touched?
- self.password = nil
- end
- end
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.url = hook_url
- hook.save
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- service_hook.execute(data, "#{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
-
- def hook_url
- url = URI.parse(jenkins_url)
- url.path = File.join(url.path || '/', "project/#{project_name}")
- url.user = ERB::Util.url_encode(username) unless username.blank?
- url.password = ERB::Util.url_encode(password) unless password.blank?
- url.to_s
- end
-
- def self.supported_events
- %w(push merge_request tag_push)
- end
-
- def title
- 'Jenkins'
- end
-
- def description
- 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_('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
- 'jenkins'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'jenkins_url',
- title: s_('ProjectService|Jenkins server URL'),
- required: true,
- placeholder: 'http://jenkins.example.com',
- help: s_('The URL of the Jenkins server.')
- },
- {
- type: 'text',
- name: 'project_name',
- required: true,
- placeholder: 'my_project_name',
- help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
- },
- {
- type: 'text',
- name: 'username',
- required: true,
- help: s_('The username for the Jenkins server.')
- },
- {
- type: 'password',
- name: 'password',
- help: s_('The password for the Jenkins server.'),
- non_empty_password_title: s_('ProjectService|Enter new password.'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
- }
- ]
- end
-end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
deleted file mode 100644
index 5cd6e79eb1d..00000000000
--- a/app/models/project_services/jira_service.rb
+++ /dev/null
@@ -1,541 +0,0 @@
-# frozen_string_literal: true
-
-# Accessible as Project#external_issue_tracker
-class JiraService < IssueTrackerService
- extend ::Gitlab::Utils::Override
- include Gitlab::Routing
- include ApplicationHelper
- include ActionView::Helpers::AssetUrlHelper
- include Gitlab::Utils::StrongMemoize
-
- PROJECTS_PER_PAGE = 50
-
- # TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
- DEPLOYMENT_TYPES = {
- server: 'SERVER',
- cloud: 'CLOUD'
- }.freeze
-
- validates :url, public_url: true, presence: true, if: :activated?
- validates :api_url, public_url: true, allow_blank: true
- validates :username, presence: true, if: :activated?
- validates :password, presence: true, if: :activated?
-
- validates :jira_issue_transition_id,
- format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") },
- allow_blank: true
-
- # Jira Cloud version is deprecating authentication via username and password.
- # We should use username/password for Jira Server and email/api_token for Jira Cloud,
- # for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
-
- # TODO: we can probably just delegate as part of
- # https://gitlab.com/gitlab-org/gitlab/issues/29404
- data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
- :vulnerabilities_enabled, :vulnerabilities_issuetype
-
- before_update :reset_password
- after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
-
- enum comment_detail: {
- standard: 1,
- all_details: 2
- }
-
- alias_method :project_url, :url
-
- # When these are false GitLab does not create cross reference
- # comments on Jira except when an issue gets transitioned.
- def self.supported_events
- %w(commit merge_request)
- end
-
- def self.supported_event_actions
- %w(comment)
- end
-
- # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def self.reference_pattern(only_long: true)
- @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
- end
-
- def initialize_properties
- {}
- end
-
- def data_fields
- jira_tracker_data || self.build_jira_tracker_data
- end
-
- def reset_password
- data_fields.password = nil if reset_password?
- end
-
- def set_default_data
- return unless issues_tracker.present?
-
- return if url
-
- data_fields.url ||= issues_tracker['url']
- data_fields.api_url ||= issues_tracker['api_url']
- end
-
- def options
- url = URI.parse(client_url)
-
- {
- username: username&.strip,
- password: password,
- site: URI.join(url, '/').to_s, # Intended to find the root
- context_path: url.path,
- auth_type: :basic,
- read_timeout: 120,
- use_cookies: true,
- additional_cookies: ['OBBasicAuth=fromDialog'],
- use_ssl: url.scheme == 'https'
- }
- end
-
- def client
- @client ||= begin
- JIRA::Client.new(options).tap do |client|
- # Replaces JIRA default http client with our implementation
- client.request_client = Gitlab::Jira::HttpClient.new(client.options)
- end
- end
- end
-
- def help
- 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
- 'Jira'
- end
-
- def description
- s_("JiraService|Use Jira as this project's issue tracker.")
- end
-
- def self.to_param
- 'jira'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'url',
- title: s_('JiraService|Web URL'),
- placeholder: 'https://jira.example.com',
- help: s_('JiraService|Base URL of the Jira instance.'),
- required: true
- },
- {
- type: 'text',
- name: 'api_url',
- title: s_('JiraService|Jira API URL'),
- help: s_('JiraService|If different from Web URL.')
- },
- {
- type: 'text',
- name: 'username',
- title: s_('JiraService|Username or Email'),
- help: s_('JiraService|Use a username for server version and an email for cloud version.'),
- required: true
- },
- {
- type: 'password',
- name: 'password',
- title: s_('JiraService|Password or API token'),
- non_empty_password_title: s_('JiraService|Enter new password or API token'),
- non_empty_password_help: s_('JiraService|Leave blank to use your current password or API token.'),
- help: s_('JiraService|Use a password for server version and an API token for cloud version.'),
- required: true
- }
- ]
- end
-
- def issues_url
- "#{url}/browse/:id"
- end
-
- def new_issue_url
- "#{url}/secure/CreateIssue!default.jspa"
- end
-
- alias_method :original_url, :url
- def url
- original_url&.delete_suffix('/')
- end
-
- alias_method :original_api_url, :api_url
- def api_url
- original_api_url&.delete_suffix('/')
- end
-
- def execute(push)
- # This method is a no-op, because currently JiraService does not
- # support any events.
- end
-
- def find_issue(issue_key, rendered_fields: false, transitions: false)
- expands = []
- expands << 'renderedFields' if rendered_fields
- expands << 'transitions' if transitions
- options = { expand: expands.join(',') } if expands.any?
-
- jira_request { client.Issue.find(issue_key, options || {}) }
- end
-
- def close_issue(entity, external_issue, current_user)
- issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)
-
- return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?
-
- commit_id = case entity
- when Commit then entity.id
- when MergeRequest then entity.diff_head_sha
- end
-
- commit_url = build_entity_url(:commit, commit_id)
-
- # Depending on the Jira project's workflow, a comment during transition
- # may or may not be allowed. Refresh the issue after transition and check
- # if it is closed, so we don't have one comment for every commit.
- issue = find_issue(issue.key) if transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
- log_usage(:close_issue, current_user)
- end
-
- def create_cross_reference_note(mentioned, noteable, author)
- unless can_cross_reference?(noteable)
- return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) }
- end
-
- jira_issue = find_issue(mentioned.id)
-
- return unless jira_issue.present?
-
- noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
- noteable_type = noteable_name(noteable)
- entity_url = build_entity_url(noteable_type, noteable_id)
- entity_meta = build_entity_meta(noteable)
-
- data = {
- user: {
- name: author.name,
- url: resource_url(user_path(author))
- },
- project: {
- name: project.full_path,
- url: resource_url(project_path(project))
- },
- entity: {
- id: entity_meta[:id],
- name: noteable_type.humanize.downcase,
- url: entity_url,
- title: noteable.title,
- description: entity_meta[:description],
- branch: entity_meta[:branch]
- }
- }
-
- add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
- end
-
- def valid_connection?
- test(nil)[:success]
- end
-
- def test(_)
- result = server_info
- success = result.present?
- result = @error&.message unless success
-
- { success: success, result: result }
- end
-
- override :support_close_issue?
- def support_close_issue?
- true
- end
-
- override :support_cross_reference?
- def support_cross_reference?
- true
- end
-
- def issue_transition_enabled?
- jira_issue_transition_automatic || jira_issue_transition_id.present?
- end
-
- private
-
- def server_info
- strong_memoize(:server_info) do
- client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
- end
- end
-
- def can_cross_reference?(noteable)
- case noteable
- when Commit then commit_events
- when MergeRequest then merge_requests_events
- else true
- end
- end
-
- # jira_issue_transition_id can have multiple values split by , or ;
- # the issue is transitioned at the order given by the user
- # if any transition fails it will log the error message and stop the transition sequence
- def transition_issue(issue)
- return transition_issue_to_done(issue) if jira_issue_transition_automatic
-
- jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id|
- transition_issue_to_id(issue, transition_id)
- end
- end
-
- def transition_issue_to_id(issue, transition_id)
- issue.transitions.build.save!(
- transition: { id: transition_id }
- )
-
- true
- rescue StandardError => error
- log_error(
- "Issue transition failed",
- error: {
- exception_class: error.class.name,
- exception_message: error.message,
- exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
- },
- client_url: client_url
- )
-
- false
- end
-
- def transition_issue_to_done(issue)
- transitions = issue.transitions rescue []
-
- transition = transitions.find do |transition|
- status = transition&.to&.statusCategory
- status && status['key'] == 'done'
- end
-
- return false unless transition
-
- transition_issue_to_id(issue, transition.id)
- end
-
- def log_usage(action, user)
- key = "i_ecosystem_jira_service_#{action}"
-
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id)
- end
-
- def add_issue_solved_comment(issue, commit_id, commit_url)
- link_title = "Solved by commit #{commit_id}."
- comment = "Issue solved with [#{commit_id}|#{commit_url}]."
- link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
- send_message(issue, comment, link_props)
- end
-
- def add_comment(data, issue)
- entity_name = data[:entity][:name]
- entity_url = data[:entity][:url]
- entity_title = data[:entity][:title]
-
- message = comment_message(data)
- link_title = "#{entity_name.capitalize} - #{entity_title}"
- link_props = build_remote_link_props(url: entity_url, title: link_title)
-
- unless comment_exists?(issue, message)
- send_message(issue, message, link_props)
- end
- end
-
- def comment_message(data)
- user_link = build_jira_link(data[:user][:name], data[:user][:url])
-
- entity = data[:entity]
- entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}"
- entity_link = build_jira_link(entity_ref, entity[:url])
-
- project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
- branch =
- if entity[:branch].present?
- s_('JiraService| on branch %{branch_link}') % {
- branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
- }
- end
-
- entity_message = entity[:description].presence if all_details?
- entity_message ||= entity[:title].chomp
-
- s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
- user_link: user_link,
- entity_link: entity_link,
- project_link: project_link,
- branch: branch,
- entity_message: entity_message
- }
- end
-
- def build_jira_link(title, url)
- "[#{title}|#{url}]"
- end
-
- def has_resolution?(issue)
- issue.respond_to?(:resolution) && issue.resolution.present?
- end
-
- def comment_exists?(issue, message)
- comments = jira_request { issue.comments }
-
- comments.present? && comments.any? { |comment| comment.body.include?(message) }
- end
-
- def send_message(issue, message, remote_link_props)
- return unless client_url.present?
-
- jira_request do
- remote_link = find_remote_link(issue, remote_link_props[:object][:url])
-
- create_issue_comment(issue, message) unless remote_link
- remote_link ||= issue.remotelink.build
- remote_link.save!(remote_link_props)
-
- log_info("Successfully posted", client_url: client_url)
- "SUCCESS: Successfully posted to #{client_url}."
- end
- end
-
- def create_issue_comment(issue, message)
- return unless comment_on_event_enabled
-
- issue.comments.build.save!(body: message)
- end
-
- def find_remote_link(issue, url)
- links = jira_request { issue.remotelink.all }
- return unless links
-
- links.find { |link| link.object["url"] == url }
- end
-
- def build_remote_link_props(url:, title:, resolved: false)
- status = {
- resolved: resolved
- }
-
- {
- GlobalID: 'GitLab',
- relationship: 'mentioned on',
- object: {
- url: url,
- title: title,
- status: status,
- icon: {
- title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.base_url)
- }
- }
- }
- end
-
- def resource_url(resource)
- "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
- end
-
- def build_entity_url(noteable_type, entity_id)
- polymorphic_url(
- [
- self.project,
- noteable_type.to_sym
- ],
- id: entity_id,
- host: Settings.gitlab.base_url
- )
- end
-
- def build_entity_meta(noteable)
- if noteable.is_a?(Commit)
- {
- id: noteable.short_id,
- description: noteable.safe_message,
- branch: noteable.ref_names(project.repository).first
- }
- elsif noteable.is_a?(MergeRequest)
- {
- id: noteable.to_reference,
- branch: noteable.source_branch
- }
- else
- {}
- end
- end
-
- def noteable_name(noteable)
- name = noteable.model_name.singular
-
- # ProjectSnippet inherits from Snippet class so it causes
- # routing error building the URL.
- name == "project_snippet" ? "snippet" : name
- end
-
- # Handle errors when doing Jira API calls
- def jira_request
- yield
- rescue StandardError => error
- @error = error
- log_error("Error sending message", client_url: client_url, error: @error.message)
- nil
- end
-
- def client_url
- api_url.presence || url
- end
-
- def reset_password?
- # don't reset the password if a new one is provided
- return false if password_touched?
- return true if api_url_changed?
- return false if api_url.present?
-
- url_changed?
- end
-
- def update_deployment_type?
- (api_url_changed? || url_changed? || username_changed? || password_changed?) &&
- can_test?
- end
-
- def update_deployment_type
- clear_memoization(:server_info) # ensure we run the request when we try to update deployment type
- results = server_info
- return data_fields.deployment_unknown! unless results.present?
-
- case results['deploymentType']
- when 'Server'
- data_fields.deployment_server!
- when 'Cloud'
- data_fields.deployment_cloud!
- else
- data_fields.deployment_unknown!
- end
- end
-
- def self.event_description(event)
- case event
- when "merge_request", "merge_request_events"
- s_("JiraService|Jira comments will be created when an issue gets referenced in a merge request.")
- when "commit", "commit_events"
- s_("JiraService|Jira comments will be created when an issue gets referenced in a commit.")
- end
- end
-end
-
-JiraService.prepend_mod_with('JiraService')
diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb
deleted file mode 100644
index 2c145abf5c9..00000000000
--- a/app/models/project_services/jira_tracker_data.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-class JiraTrackerData < ApplicationRecord
- include Services::DataFields
- include IgnorableColumns
-
- ignore_columns %i[
- encrypted_proxy_address
- encrypted_proxy_address_iv
- encrypted_proxy_port
- encrypted_proxy_port_iv
- encrypted_proxy_username
- encrypted_proxy_username_iv
- encrypted_proxy_password
- encrypted_proxy_password_iv
- ], remove_with: '14.0', remove_after: '2021-05-22'
-
- attr_encrypted :url, encryption_options
- attr_encrypted :api_url, encryption_options
- attr_encrypted :username, encryption_options
- attr_encrypted :password, encryption_options
-
- enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment
-end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
deleted file mode 100644
index 732a7c32a03..00000000000
--- a/app/models/project_services/mattermost_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-class MattermostService < ChatNotificationService
- include SlackMattermost::Notifier
- include ActionView::Helpers::UrlHelper
-
- def title
- s_('Mattermost notifications')
- end
-
- def description
- s_('Send notifications about project events to Mattermost channels.')
- end
-
- def self.to_param
- 'mattermost'
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
- s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def default_channel_placeholder
- 'my-channel'
- end
-
- def webhook_placeholder
- 'http://mattermost.example.com/hooks/'
- end
-end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
deleted file mode 100644
index 60235a09dcd..00000000000
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class MattermostSlashCommandsService < SlashCommandsService
- include Ci::TriggersHelper
-
- prop_accessor :token
-
- def can_test?
- false
- end
-
- def title
- 'Mattermost slash commands'
- end
-
- def description
- "Perform common tasks with slash commands."
- end
-
- def self.to_param
- 'mattermost_slash_commands'
- end
-
- def configure(user, params)
- token = Mattermost::Command.new(user)
- .create(command(params))
-
- update(active: true, token: token) if token
- rescue Mattermost::Error => e
- [false, e.message]
- end
-
- def list_teams(current_user)
- [Mattermost::Team.new(current_user).all, nil]
- rescue Mattermost::Error => e
- [[], e.message]
- end
-
- def chat_responder
- ::Gitlab::Chat::Responder::Mattermost
- end
-
- private
-
- def command(params)
- pretty_project_name = project.full_name
-
- params.merge(
- auto_complete: true,
- auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
- auto_complete_hint: '[help]',
- description: "Perform common operations on: #{pretty_project_name}",
- display_name: "GitLab / #{pretty_project_name}",
- method: 'P',
- username: 'GitLab')
- end
-end
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
deleted file mode 100644
index 1d2067067da..00000000000
--- a/app/models/project_services/microsoft_teams_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class MicrosoftTeamsService < ChatNotificationService
- def title
- 'Microsoft Teams notifications'
- end
-
- def description
- 'Send notifications about project events to Microsoft Teams.'
- end
-
- def self.to_param
- 'microsoft_teams'
- end
-
- def help
- '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>'
- end
-
- def webhook_placeholder
- 'https://outlook.office.com/webhook/…'
- end
-
- def event_field(event)
- end
-
- def default_channel_placeholder
- end
-
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
- { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'If selected, successful pipelines do not trigger a notification event.' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
- ]
- end
-
- private
-
- def notify(message, opts)
- MicrosoftTeams::Notifier.new(webhook).ping(
- title: message.project_name,
- summary: message.summary,
- activity: message.activity,
- attachments: message.attachments
- )
- end
-
- def custom_data(data)
- super(data).merge(markdown: true)
- end
-end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
deleted file mode 100644
index bd6344c6e1a..00000000000
--- a/app/models/project_services/mock_ci_service.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
-class MockCiService < CiService
- ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
-
- prop_accessor :mock_service_url
- validates :mock_service_url, presence: true, public_url: true, if: :activated?
-
- def title
- 'MockCI'
- end
-
- def description
- 'Mock an external CI'
- end
-
- def self.to_param
- 'mock_ci'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'mock_service_url',
- title: s_('ProjectService|Mock service URL'),
- placeholder: 'http://localhost:4004',
- required: true
- }
- ]
- end
-
- # Return complete url to build page
- #
- # Ex.
- # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
- #
- def build_page(sha, ref)
- Gitlab::Utils.append_path(
- mock_service_url,
- "#{project.namespace.path}/#{project.path}/status/#{sha}")
- end
-
- # Return string with build status or :error symbol
- #
- # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
- #
- #
- # Ex.
- # @service.commit_status('13be4ac', 'master')
- # # => 'success'
- #
- # @service.commit_status('2abe4ac', 'dev')
- # # => 'running'
- #
- #
- def commit_status(sha, ref)
- response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
- read_commit_status(response)
- rescue Errno::ECONNREFUSED
- :error
- end
-
- def commit_status_path(sha)
- Gitlab::Utils.append_path(
- mock_service_url,
- "#{project.namespace.path}/#{project.path}/status/#{sha}.json")
- end
-
- def read_commit_status(response)
- return :error unless response.code == 200 || response.code == 404
-
- status = if response.code == 404
- 'pending'
- else
- response['status']
- end
-
- if status.present? && ALLOWED_STATES.include?(status)
- status
- else
- :error
- end
- end
-
- def can_test?
- false
- end
-end
diff --git a/app/models/project_services/open_project_service.rb b/app/models/project_services/open_project_service.rb
deleted file mode 100644
index a24fbc1611d..00000000000
--- a/app/models/project_services/open_project_service.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class OpenProjectService < IssueTrackerService
- validates :url, public_url: true, presence: true, if: :activated?
- validates :api_url, public_url: true, allow_blank: true, if: :activated?
- validates :token, presence: true, if: :activated?
- validates :project_identifier_code, presence: true, if: :activated?
-
- data_field :url, :api_url, :token, :closed_status_id, :project_identifier_code
-
- def data_fields
- open_project_tracker_data || self.build_open_project_tracker_data
- end
-
- def self.to_param
- 'open_project'
- end
-end
diff --git a/app/models/project_services/open_project_tracker_data.rb b/app/models/project_services/open_project_tracker_data.rb
deleted file mode 100644
index 20de60e40c1..00000000000
--- a/app/models/project_services/open_project_tracker_data.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class OpenProjectTrackerData < ApplicationRecord
- include Services::DataFields
-
- # When the Open Project is fresh installed, the default closed status id is "13" based on current version: v8.
- DEFAULT_CLOSED_STATUS_ID = "13"
-
- attr_encrypted :url, encryption_options
- attr_encrypted :api_url, encryption_options
- attr_encrypted :token, encryption_options
-
- def closed_status_id
- super || DEFAULT_CLOSED_STATUS_ID
- end
-end
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
deleted file mode 100644
index f3ea8c64302..00000000000
--- a/app/models/project_services/packagist_service.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-class PackagistService < Integration
- prop_accessor :username, :token, :server
-
- validates :username, presence: true, if: :activated?
- validates :token, presence: true, if: :activated?
-
- default_value_for :push_events, true
- default_value_for :tag_push_events, true
-
- after_save :compose_service_hook, if: :activated?
-
- def title
- 'Packagist'
- end
-
- def description
- s_('Integrations|Update your Packagist projects.')
- end
-
- def self.to_param
- 'packagist'
- end
-
- def fields
- [
- { type: 'text', name: 'username', placeholder: '', required: true },
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
- ]
- end
-
- def self.supported_events
- %w(push merge_request tag_push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- service_hook.execute(data)
- end
-
- def test(data)
- begin
- result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 202
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result[:message] }
- end
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.url = hook_url
- hook.save
- end
-
- def hook_url
- base_url = server.presence || 'https://packagist.org'
- "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}"
- end
-end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
deleted file mode 100644
index 4603193ac8e..00000000000
--- a/app/models/project_services/pipelines_email_service.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# frozen_string_literal: true
-
-class PipelinesEmailService < Integration
- include NotificationBranchSelection
-
- prop_accessor :recipients, :branches_to_be_notified
- boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
- validates :recipients, presence: true, if: :validate_recipients?
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_pipelines = true
- self.branches_to_be_notified = "default"
- 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
- # `branches_to_be_notified`. Instead of doing a background migration, we
- # opted to set a value for the new property based on the old one, if
- # users hasn't specified one already. When users edit the service and
- # selects a value for this new property, it will override everything.
-
- self.branches_to_be_notified ||= notify_only_default_branch? ? "default" : "all"
- end
- end
-
- def title
- _('Pipeline status emails')
- end
-
- def description
- _('Email the pipeline status to a list of recipients.')
- end
-
- def self.to_param
- 'pipelines_email'
- end
-
- def self.supported_events
- %w[pipeline]
- end
-
- def self.default_test_event
- 'pipeline'
- end
-
- def execute(data, force: false)
- return unless supported_events.include?(data[:object_kind])
- return unless force || should_pipeline_be_notified?(data)
-
- all_recipients = retrieve_recipients(data)
-
- return unless all_recipients.any?
-
- pipeline_id = data[:object_attributes][:id]
- PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients)
- end
-
- def can_test?
- project&.ci_pipelines&.any?
- end
-
- def fields
- [
- { type: 'textarea',
- name: 'recipients',
- help: _('Comma-separated list of email addresses.'),
- required: true },
- { type: 'checkbox',
- name: 'notify_only_broken_pipelines' },
- { type: 'select',
- name: 'branches_to_be_notified',
- choices: branch_choices }
- ]
- end
-
- def test(data)
- result = execute(data, force: true)
-
- { success: true, result: result }
- rescue StandardError => error
- { success: false, result: error }
- end
-
- def should_pipeline_be_notified?(data)
- notify_for_branch?(data) && notify_for_pipeline?(data)
- end
-
- def notify_for_pipeline?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
- end
-
- def retrieve_recipients(data)
- recipients.to_s.split(/[,\r\n ]+/).reject(&:empty?)
- end
-end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
deleted file mode 100644
index 6e67984591d..00000000000
--- a/app/models/project_services/pivotaltracker_service.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-class PivotaltrackerService < Integration
- API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
-
- prop_accessor :token, :restrict_to_branch
- validates :token, presence: true, if: :activated?
-
- def title
- 'PivotalTracker'
- end
-
- def description
- s_('PivotalTrackerService|Add commit messages as comments to PivotalTracker stories.')
- end
-
- def self.to_param
- 'pivotaltracker'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'token',
- placeholder: s_('PivotalTrackerService|Pivotal Tracker API token.'),
- required: true
- },
- {
- type: 'text',
- name: 'restrict_to_branch',
- placeholder: s_('PivotalTrackerService|Comma-separated list of branches which will be ' \
- 'automatically inspected. Leave blank to include all branches.')
- }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
- return unless allowed_branch?(data[:ref])
-
- data[:commits].each do |commit|
- message = {
- 'source_commit' => {
- 'commit_id' => commit[:id],
- 'author' => commit[:author][:name],
- 'url' => commit[:url],
- 'message' => commit[:message]
- }
- }
- Gitlab::HTTP.post(
- API_ENDPOINT,
- body: message.to_json,
- headers: {
- 'Content-Type' => 'application/json',
- 'X-TrackerToken' => token
- }
- )
- end
- end
-
- private
-
- def allowed_branch?(ref)
- return true unless ref.present? && restrict_to_branch.present?
-
- branch = Gitlab::Git.ref_name(ref)
- allowed_branches = restrict_to_branch.split(',').map(&:strip)
-
- branch.present? && allowed_branches.include?(branch)
- end
-end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index b8869547a37..a289c1c2afb 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -117,8 +117,8 @@ class PrometheusService < MonitoringService
return false if template?
return false unless project
- project.all_clusters.enabled.eager_load(:application_prometheus).any? do |cluster|
- cluster.application_prometheus&.available?
+ project.all_clusters.enabled.eager_load(:integration_prometheus).any? do |cluster|
+ cluster.integration_prometheus_available?
end
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
deleted file mode 100644
index 89765fbdf41..00000000000
--- a/app/models/project_services/pushover_service.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-class PushoverService < Integration
- BASE_URI = 'https://api.pushover.net/1'
-
- prop_accessor :api_key, :user_key, :device, :priority, :sound
- validates :api_key, :user_key, :priority, presence: true, if: :activated?
-
- def title
- 'Pushover'
- end
-
- def description
- s_('PushoverService|Get real-time notifications on your device.')
- end
-
- def self.to_param
- 'pushover'
- end
-
- def fields
- [
- { type: 'text', name: 'api_key', title: _('API key'), placeholder: s_('PushoverService|Your application key'), required: true },
- { type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true },
- { type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') },
- { type: 'select', name: 'priority', required: true, choices:
- [
- [s_('PushoverService|Lowest Priority'), -2],
- [s_('PushoverService|Low Priority'), -1],
- [s_('PushoverService|Normal Priority'), 0],
- [s_('PushoverService|High Priority'), 1]
- ],
- default_choice: 0 },
- { type: 'select', name: 'sound', choices:
- [
- ['Device default sound', nil],
- ['Pushover (default)', 'pushover'],
- %w(Bike bike),
- %w(Bugle bugle),
- ['Cash Register', 'cashregister'],
- %w(Classical classical),
- %w(Cosmic cosmic),
- %w(Falling falling),
- %w(Gamelan gamelan),
- %w(Incoming incoming),
- %w(Intermission intermission),
- %w(Magic magic),
- %w(Mechanical mechanical),
- ['Piano Bar', 'pianobar'],
- %w(Siren siren),
- ['Space Alarm', 'spacealarm'],
- ['Tug Boat', 'tugboat'],
- ['Alien Alarm (long)', 'alien'],
- ['Climb (long)', 'climb'],
- ['Persistent (long)', 'persistent'],
- ['Pushover Echo (long)', 'echo'],
- ['Up Down (long)', 'updown'],
- ['None (silent)', 'none']
- ] }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- ref = Gitlab::Git.ref_name(data[:ref])
- before = data[:before]
- after = data[:after]
-
- message =
- if Gitlab::Git.blank_ref?(before)
- s_("PushoverService|%{user_name} pushed new branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref }
- elsif Gitlab::Git.blank_ref?(after)
- s_("PushoverService|%{user_name} deleted branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref }
- else
- s_("PushoverService|%{user_name} push to branch \"%{ref}\".") % { user_name: data[:user_name], ref: ref }
- end
-
- if data[:total_commits_count] > 0
- message = [message, s_("PushoverService|Total commits count: %{total_commits_count}") % { total_commits_count: data[:total_commits_count] }].join("\n")
- end
-
- pushover_data = {
- token: api_key,
- user: user_key,
- device: device,
- priority: priority,
- title: "#{project.full_name}",
- message: message,
- url: data[:project][:web_url],
- url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name }
- }
-
- # Sound parameter MUST NOT be sent to API if not selected
- if sound
- pushover_data[:sound] = sound
- end
-
- Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
- end
-end
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
deleted file mode 100644
index 7a0f500209c..00000000000
--- a/app/models/project_services/redmine_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-class RedmineService < IssueTrackerService
- include ActionView::Helpers::UrlHelper
- validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
-
- def title
- 'Redmine'
- end
-
- def description
- s_("IssueTracker|Use Redmine 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/redmine'), target: '_blank', rel: 'noopener noreferrer'
- s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'redmine'
- end
-end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
deleted file mode 100644
index 92a46f8d01f..00000000000
--- a/app/models/project_services/slack_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class SlackService < ChatNotificationService
- include SlackMattermost::Notifier
- extend ::Gitlab::Utils::Override
-
- SUPPORTED_EVENTS_FOR_USAGE_LOG = %w[
- push issue confidential_issue merge_request note confidential_note
- tag_push wiki_page deployment
- ].freeze
-
- prop_accessor EVENT_CHANNEL['alert']
-
- def title
- 'Slack notifications'
- end
-
- def description
- 'Send notifications about project events to Slack.'
- end
-
- def self.to_param
- 'slack'
- end
-
- def default_channel_placeholder
- _('general, development')
- end
-
- def webhook_placeholder
- 'https://hooks.slack.com/services/…'
- end
-
- def supported_events
- additional = []
- additional << 'alert'
-
- super + additional
- end
-
- def get_message(object_kind, data)
- return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
-
- super
- end
-
- override :log_usage
- def log_usage(event, user_id)
- return unless user_id
-
- return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
-
- key = "i_ecosystem_slack_service_#{event}_notification"
-
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
- end
-end
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
deleted file mode 100644
index 548f3623504..00000000000
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-class SlackSlashCommandsService < SlashCommandsService
- include Ci::TriggersHelper
-
- def title
- 'Slack slash commands'
- end
-
- def description
- "Perform common operations in Slack"
- end
-
- def self.to_param
- 'slack_slash_commands'
- end
-
- def trigger(params)
- # Format messages to be Slack-compatible
- super.tap do |result|
- result[:text] = format(result[:text]) if result.is_a?(Hash)
- end
- end
-
- def chat_responder
- ::Gitlab::Chat::Responder::Slack
- end
-
- private
-
- def format(text)
- Slack::Messenger::Util::LinkFormatter.format(text) if text
- end
-end
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
deleted file mode 100644
index 37d16737052..00000000000
--- a/app/models/project_services/slash_commands_service.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-# Base class for Chat services
-# This class is not meant to be used directly, but only to inherrit from.
-class SlashCommandsService < Integration
- default_value_for :category, 'chat'
-
- prop_accessor :token
-
- has_many :chat_names, foreign_key: :service_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
- def valid_token?(token)
- self.respond_to?(:token) &&
- self.token.present? &&
- ActiveSupport::SecurityUtils.secure_compare(token, self.token)
- end
-
- def self.supported_events
- %w()
- end
-
- def can_test?
- false
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
- ]
- end
-
- def trigger(params)
- return unless valid_token?(params[:token])
-
- chat_user = find_chat_user(params)
- user = chat_user&.user
-
- if user
- unless user.can?(:use_slash_commands)
- return Gitlab::SlashCommands::Presenters::Access.new.deactivated if user.deactivated?
-
- return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project)
- end
-
- Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
- else
- url = authorize_chat_name_url(params)
- Gitlab::SlashCommands::Presenters::Access.new(url).authorize
- end
- end
-
- private
-
- # rubocop: disable CodeReuse/ServiceClass
- def find_chat_user(params)
- ChatNames::FindUserService.new(self, params).execute
- end
- # rubocop: enable CodeReuse/ServiceClass
-
- # rubocop: disable CodeReuse/ServiceClass
- def authorize_chat_name_url(params)
- ChatNames::AuthorizeUserService.new(self, params).execute
- end
- # rubocop: enable CodeReuse/ServiceClass
-end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
deleted file mode 100644
index 6fc24a4778c..00000000000
--- a/app/models/project_services/teamcity_service.rb
+++ /dev/null
@@ -1,189 +0,0 @@
-# frozen_string_literal: true
-
-class TeamcityService < CiService
- include ReactiveService
- include ServicePushDataValidations
-
- prop_accessor :teamcity_url, :build_type, :username, :password
-
- validates :teamcity_url, presence: true, public_url: true, if: :activated?
- validates :build_type, 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
-
- class << self
- def to_param
- 'teamcity'
- end
-
- def supported_events
- %w(push merge_request)
- end
-
- def event_description(event)
- case event
- when 'push', 'push_events'
- 'TeamCity CI will be triggered after every push to the repository except branch delete'
- when 'merge_request', 'merge_request_events'
- 'TeamCity CI will be triggered after a merge request has been created or updated'
- end
- end
- end
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.save
- end
-
- def reset_password
- if teamcity_url_changed? && !password_touched?
- self.password = nil
- end
- end
-
- def title
- 'JetBrains TeamCity'
- end
-
- def description
- s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
- end
-
- def help
- s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'teamcity_url',
- title: s_('ProjectService|TeamCity server URL'),
- placeholder: 'https://teamcity.example.com',
- required: true
- },
- {
- type: 'text',
- name: 'build_type',
- help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
- required: true
- },
- {
- type: 'text',
- name: 'username',
- help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
- },
- {
- 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 calculate_reactive_cache(sha, ref)
- response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}")
-
- if response
- { build_page: read_build_page(response), commit_status: read_commit_status(response) }
- else
- { build_page: teamcity_url, commit_status: :error }
- end
- end
-
- def execute(data)
- case data[:object_kind]
- when 'push'
- execute_push(data)
- when 'merge_request'
- execute_merge_request(data)
- end
- end
-
- private
-
- def execute_push(data)
- branch = Gitlab::Git.ref_name(data[:ref])
- post_to_build_queue(data, branch) if push_valid?(data)
- end
-
- def execute_merge_request(data)
- branch = data[:object_attributes][:source_branch]
- post_to_build_queue(data, branch) if merge_request_valid?(data)
- end
-
- def read_build_page(response)
- if response.code != 200
- # If actual build link can't be determined,
- # send user to build summary page.
- build_url("viewLog.html?buildTypeId=#{build_type}")
- else
- # If actual build link is available, go to build result page.
- built_id = response['build']['id']
- build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
- end
- end
-
- def read_commit_status(response)
- return :error unless response.code == 200 || response.code == 404
-
- status = if response.code == 404
- 'Pending'
- else
- response['build']['status']
- end
-
- return :error unless status.present?
-
- if status.include?('SUCCESS')
- 'success'
- elsif status.include?('FAILURE')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
- end
-
- def build_url(path)
- Gitlab::Utils.append_path(teamcity_url, path)
- end
-
- def get_path(path)
- Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
- end
-
- def post_to_build_queue(data, branch)
- Gitlab::HTTP.post(
- build_url('httpAuth/app/rest/buildQueue'),
- body: "<build branchName=#{branch.encode(xml: :attr)}>"\
- "<buildType id=#{build_type.encode(xml: :attr)}/>"\
- '</build>',
- headers: { 'Content-type' => 'application/xml' },
- basic_auth: basic_auth
- )
- end
-
- def basic_auth
- { username: username, password: password }
- end
-end
diff --git a/app/models/project_services/unify_circuit_service.rb b/app/models/project_services/unify_circuit_service.rb
deleted file mode 100644
index 5f43388e1c9..00000000000
--- a/app/models/project_services/unify_circuit_service.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-class UnifyCircuitService < ChatNotificationService
- def title
- 'Unify Circuit'
- end
-
- def description
- s_('Integrations|Send notifications about project events to Unify Circuit.')
- end
-
- def self.to_param
- 'unify_circuit'
- end
-
- def help
- 'This service sends notifications about projects events to a Unify Circuit conversation.<br />
- To set up this service:
- <ol>
- <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">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>'
- end
-
- def event_field(event)
- end
-
- def default_channel_placeholder
- end
-
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
- end
-
- def default_fields
- [
- { type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
- ]
- end
-
- private
-
- def notify(message, opts)
- response = Gitlab::HTTP.post(webhook, body: {
- subject: message.project_name,
- text: message.summary,
- markdown: true
- }.to_json)
-
- response if response.success?
- end
-
- def custom_data(data)
- super(data).merge(markdown: true)
- end
-end
diff --git a/app/models/project_services/webex_teams_service.rb b/app/models/project_services/webex_teams_service.rb
deleted file mode 100644
index 3d92d3bb85e..00000000000
--- a/app/models/project_services/webex_teams_service.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-class WebexTeamsService < ChatNotificationService
- include ActionView::Helpers::UrlHelper
-
- def title
- s_("WebexTeamsService|Webex Teams")
- end
-
- def description
- s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
- end
-
- def self.to_param
- 'webex_teams'
- end
-
- def help
- 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)
- end
-
- def default_channel_placeholder
- end
-
- def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
- end
-
- def default_fields
- [
- { 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 }
- ]
- end
-
- private
-
- def notify(message, opts)
- header = { 'Content-Type' => 'application/json' }
- response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
-
- response if response.success?
- end
-
- def custom_data(data)
- super(data).merge(markdown: true)
- end
-end
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
deleted file mode 100644
index 9760a22a872..00000000000
--- a/app/models/project_services/youtrack_service.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# 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
- def self.reference_pattern(only_long: false)
- if only_long
- /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)/
- else
- /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})/
- end
- end
-
- def title
- 'YouTrack'
- end
-
- def description
- 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
- 'youtrack'
- end
-
- def fields
- [
- { 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_statistics.rb b/app/models/project_statistics.rb
index 37ddd2d030d..387732cf151 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -94,18 +94,14 @@ class ProjectStatistics < ApplicationRecord
end
def update_storage_size
- storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size
- # The `snippets_size` column was added on 20200622095419 but db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
- # might try to update project statistics before the `snippets_size` column has been created.
- storage_size += snippets_size if self.class.column_names.include?('snippets_size')
-
- # The `pipeline_artifacts_size` column was added on 20200817142800 but db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
- # might try to update project statistics before the `pipeline_artifacts_size` column has been created.
- storage_size += pipeline_artifacts_size if self.class.column_names.include?('pipeline_artifacts_size')
-
- # The `uploads_size` column was added on 20201105021637 but db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
- # might try to update project statistics before the `uploads_size` column has been created.
- storage_size += uploads_size if self.class.column_names.include?('uploads_size')
+ storage_size = repository_size +
+ wiki_size +
+ lfs_objects_size +
+ build_artifacts_size +
+ packages_size +
+ snippets_size +
+ pipeline_artifacts_size +
+ uploads_size
self.storage_size = storage_size
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 889eaed138d..3df8fe31826 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -30,8 +30,6 @@ class ProtectedBranch < ApplicationRecord
end
def self.allow_force_push?(project, ref_name)
- return false unless ::Feature.enabled?(:allow_force_push_to_protected_branches, project, default_enabled: :yaml)
-
project.protected_branches.allowing_force_push.matching(ref_name).any?
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 1889a0707b4..aad1cbeabdb 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -39,10 +39,10 @@ class Release < ApplicationRecord
scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) }
# Sorting
- scope :order_created, -> { reorder('created_at ASC') }
- scope :order_created_desc, -> { reorder('created_at DESC') }
- scope :order_released, -> { reorder('released_at ASC') }
- scope :order_released_desc, -> { reorder('released_at DESC') }
+ scope :order_created, -> { reorder(created_at: :asc) }
+ scope :order_created_desc, -> { reorder(created_at: :desc) }
+ scope :order_released, -> { reorder(released_at: :asc) }
+ scope :order_released_desc, -> { reorder(released_at: :desc) }
delegate :repository, to: :project
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 9c30d0611e6..84e0a43670b 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -33,7 +33,7 @@ class ReleaseHighlight
next unless include_item?(item)
begin
- item.tap {|i| i['body'] = Kramdown::Document.new(i['body']).to_html }
+ item.tap {|i| i['body'] = Banzai.render(i['body'], { project: nil }) }
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, file_path: file_path)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7dca8e52403..1bd61fe48cb 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -938,6 +938,8 @@ class Repository
end
def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true)
+ return fetch_remote(remote_name, url: url, refmap: refmap, forced: forced, prune: prune) if Feature.enabled?(:fetch_remote_params, project, default_enabled: :yaml)
+
unless remote_name
remote_name = "tmp-#{SecureRandom.hex}"
tmp_remote_name = true
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index bcc17d32272..c5203354b9d 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -6,9 +6,12 @@ class ServiceDeskSetting < ApplicationRecord
belongs_to :project
validates :project_id, presence: true
validate :valid_issue_template
+ validate :valid_project_key
validates :outgoing_name, length: { maximum: 255 }, allow_blank: true
validates :project_key, length: { maximum: 255 }, allow_blank: true, format: { with: /\A[a-z0-9_]+\z/ }
+ scope :with_project_key, ->(key) { where(project_key: key) }
+
def issue_template_content
strong_memoize(:issue_template_content) do
next unless issue_template_key.present?
@@ -27,4 +30,23 @@ class ServiceDeskSetting < ApplicationRecord
errors.add(:issue_template_key, 'is empty or does not exist')
end
end
+
+ def valid_project_key
+ if projects_with_same_slug_and_key_exists?
+ errors.add(:project_key, 'already in use for another service desk address.')
+ end
+ end
+
+ private
+
+ def projects_with_same_slug_and_key_exists?
+ return false unless project_key
+
+ settings = self.class.with_project_key(project_key).preload(:project)
+ project_slug = self.project.full_path_slug
+
+ settings.any? do |setting|
+ setting.project.full_path_slug == project_slug
+ end
+ end
end
diff --git a/app/models/snippet_repository_storage_move.rb b/app/models/snippet_repository_storage_move.rb
deleted file mode 100644
index 8234905a7e1..00000000000
--- a/app/models/snippet_repository_storage_move.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-# This is a compatibility class to avoid calling a non-existent
-# class from sidekiq during deployment.
-#
-# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
-# we cannot remove this class entirely because there can be jobs
-# referencing it.
-#
-# We can get rid of this class in 14.0
-# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
-class SnippetRepositoryStorageMove < Snippets::RepositoryStorageMove
-end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index bd543526685..96fd485b797 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -18,8 +18,12 @@ class Timelog < ApplicationRecord
joins(:project).where(projects: { namespace: group.self_and_descendants })
end
- scope :between_times, -> (start_time, end_time) do
- where('spent_at BETWEEN ? AND ?', start_time, end_time)
+ scope :at_or_after, -> (start_time) do
+ where('spent_at >= ?', start_time)
+ end
+
+ scope :at_or_before, -> (end_time) do
+ where('spent_at <= ?', end_time)
end
def issuable
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 23685fb68e0..94a99603848 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -61,6 +61,7 @@ class Todo < ApplicationRecord
scope :for_author, -> (author) { where(author: author) }
scope :for_user, -> (user) { where(user: user) }
scope :for_project, -> (projects) { where(project: projects) }
+ scope :for_note, -> (notes) { where(note: notes) }
scope :for_undeleted_projects, -> { joins(:project).merge(Project.without_deleted) }
scope :for_group, -> (group) { where(group: group) }
scope :for_type, -> (type) { where(target_type: type) }
diff --git a/app/models/user.rb b/app/models/user.rb
index 0eb58baae11..8ee0421e45f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -35,6 +35,9 @@ class User < ApplicationRecord
COUNT_CACHE_VALIDITY_PERIOD = 24.hours
+ MAX_USERNAME_LENGTH = 255
+ MIN_USERNAME_LENGTH = 2
+
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
@@ -96,12 +99,6 @@ 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
#
@@ -111,7 +108,7 @@ class User < ApplicationRecord
# Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :expired_today_and_unnotified_keys, -> { expired_today_and_not_notified }, class_name: 'Key'
+ has_many :expired_and_unnotified_keys, -> { expired_and_not_notified }, class_name: 'Key'
has_many :expiring_soon_and_unnotified_keys, -> { expiring_soon_and_not_notified }, class_name: 'Key'
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :group_deploy_keys
@@ -315,10 +312,11 @@ class User < ApplicationRecord
delegate :other_role, :other_role=, to: :user_detail, allow_nil: true
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
+ delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
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
+ accepts_nested_attributes_for :credit_card_validation, update_only: true, allow_destroy: true
state_machine :state, initial: :active do
event :block do
@@ -414,14 +412,7 @@ class User < ApplicationRecord
.without_impersonation
.expired_today_and_not_notified)
end
- scope :with_ssh_key_expired_today, -> do
- includes(:expired_today_and_unnotified_keys)
- .where('EXISTS (?)',
- ::Key
- .select(1)
- .where('keys.user_id = users.id')
- .expired_today_and_not_notified)
- end
+
scope :with_ssh_key_expiring_soon, -> do
includes(:expiring_soon_and_unnotified_keys)
.where('EXISTS (?)',
@@ -791,6 +782,16 @@ class User < ApplicationRecord
end
end
+ def automation_bot
+ email_pattern = "automation%s@#{Settings.gitlab.host}"
+
+ unique_internal(where(user_type: :automation_bot), 'automation-bot', email_pattern) do |u|
+ u.bio = 'The GitLab automation bot used for automated workflows and tasks'
+ u.name = 'GitLab Automation Bot'
+ u.avatar = bot_avatar(image: 'support-bot.png') # todo: add an avatar for automation-bot
+ end
+ end
+
# Return true if there is only single non-internal user in the deployment,
# ghost user is ignored.
def single_user?
@@ -1703,12 +1704,6 @@ 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
@@ -1928,6 +1923,20 @@ class User < ApplicationRecord
confirmed? && !blocked? && !ghost?
end
+ # This attribute hosts a Ci::JobToken::Scope object which is set when
+ # the user is authenticated successfully via CI_JOB_TOKEN.
+ def ci_job_token_scope
+ Gitlab::SafeRequestStore[ci_job_token_scope_cache_key]
+ end
+
+ def set_ci_job_token_scope!(job)
+ Gitlab::SafeRequestStore[ci_job_token_scope_cache_key] = Ci::JobToken::Scope.new(job.project)
+ end
+
+ def from_ci_job_token?
+ ci_job_token_scope.present?
+ end
+
protected
# override, from Devise::Validatable
@@ -2091,6 +2100,10 @@ class User < ApplicationRecord
def update_highest_role_attribute
id
end
+
+ def ci_job_token_scope_cache_key
+ "users:#{id}:ci:job_token_scope"
+ end
end
User.prepend_mod_with('User')
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 8fc9efddac9..2e8ff1b7b49 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -16,9 +16,7 @@ class UserCallout < ApplicationRecord
tabs_position_highlight: 10,
threat_monitoring_info: 11, # EE-only
account_recovery_regular_check: 12, # EE-only
- webhooks_moved: 13,
service_templates_deprecated_callout: 14,
- admin_integrations_moved: 15,
web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
@@ -32,7 +30,8 @@ class UserCallout < ApplicationRecord
eoa_bronze_plan_banner: 28, # EE-only
pipeline_needs_banner: 29,
pipeline_needs_hover_tip: 30,
- web_ide_ci_environments_guidance: 31
+ web_ide_ci_environments_guidance: 31,
+ security_configuration_upgrade_banner: 32
}
validates :user, presence: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 458764632ed..47537e5885f 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -6,6 +6,7 @@ class UserDetail < ApplicationRecord
belongs_to :user
+ validates :pronouns, length: { maximum: 50 }
validates :job_title, length: { maximum: 200 }
validates :bio, length: { maximum: 255 }, allow_blank: true
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index 195cfe162ac..3e5e7b259d8 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -18,7 +18,8 @@ module Users
create: 0,
verify: 1,
trial: 2,
- team: 3
+ team: 3,
+ experience: 4
}, _suffix: true
scope :without_track_and_series, -> (track, series) do