summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
commit9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch)
tree70467ae3692a0e35e5ea56bcb803eb512a10bedb /app/models
parent4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff)
downloadgitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb3
-rw-r--r--app/models/application_record.rb10
-rw-r--r--app/models/application_setting.rb11
-rw-r--r--app/models/application_setting_implementation.rb13
-rw-r--r--app/models/audit_event_archived.rb10
-rw-r--r--app/models/blob.rb1
-rw-r--r--app/models/blob_viewer/dependency_manager.rb4
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/bulk_imports/entity.rb19
-rw-r--r--app/models/bulk_imports/stage.rb65
-rw-r--r--app/models/bulk_imports/tracker.rb25
-rw-r--r--app/models/ci/build.rb53
-rw-r--r--app/models/ci/build_dependencies.rb3
-rw-r--r--app/models/ci/build_trace_chunk.rb4
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb2
-rw-r--r--app/models/ci/group.rb7
-rw-r--r--app/models/ci/job_artifact.rb8
-rw-r--r--app/models/ci/pipeline.rb61
-rw-r--r--app/models/ci/pipeline_artifact.rb2
-rw-r--r--app/models/ci/pipeline_schedule.rb2
-rw-r--r--app/models/ci/processable.rb8
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/ci/stage.rb11
-rw-r--r--app/models/ci/test_case.rb35
-rw-r--r--app/models/ci/test_case_failure.rb29
-rw-r--r--app/models/ci/unit_test.rb46
-rw-r--r--app/models/ci/unit_test_failure.rb29
-rw-r--r--app/models/clusters/agent_token.rb30
-rw-r--r--app/models/clusters/applications/prometheus.rb39
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb16
-rw-r--r--app/models/clusters/clusters_hierarchy.rb2
-rw-r--r--app/models/clusters/concerns/application_status.rb8
-rw-r--r--app/models/clusters/concerns/application_version.rb6
-rw-r--r--app/models/clusters/concerns/prometheus_client.rb50
-rw-r--r--app/models/clusters/integrations/prometheus.rb21
-rw-r--r--app/models/commit.rb11
-rw-r--r--app/models/commit_status.rb33
-rw-r--r--app/models/concerns/avatarable.rb1
-rw-r--r--app/models/concerns/boards/listable.rb1
-rw-r--r--app/models/concerns/bulk_member_access_load.rb26
-rw-r--r--app/models/concerns/cache_markdown_field.rb4
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb241
-rw-r--r--app/models/concerns/ci/artifactable.rb7
-rw-r--r--app/models/concerns/ci/has_status.rb13
-rw-r--r--app/models/concerns/counter_attribute.rb2
-rw-r--r--app/models/concerns/deprecated_assignee.rb2
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb2
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb4
-rw-r--r--app/models/concerns/has_repository.rb9
-rw-r--r--app/models/concerns/has_timelogs_report.rb20
-rw-r--r--app/models/concerns/integration.rb4
-rw-r--r--app/models/concerns/issuable.rb14
-rw-r--r--app/models/concerns/loaded_in_group_list.rb4
-rw-r--r--app/models/concerns/milestoneable.rb4
-rw-r--r--app/models/concerns/milestoneish.rb6
-rw-r--r--app/models/concerns/object_storable.rb10
-rw-r--r--app/models/concerns/participable.rb35
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/safe_url.rb4
-rw-r--r--app/models/concerns/sidebars/container_with_html_options.rb42
-rw-r--r--app/models/concerns/sidebars/has_active_routes.rb16
-rw-r--r--app/models/concerns/sidebars/has_hint.rb16
-rw-r--r--app/models/concerns/sidebars/has_icon.rb27
-rw-r--r--app/models/concerns/sidebars/has_pill.rb21
-rw-r--r--app/models/concerns/sidebars/positionable_list.rb37
-rw-r--r--app/models/concerns/sidebars/renderable.rb12
-rw-r--r--app/models/concerns/sortable.rb1
-rw-r--r--app/models/concerns/subscribable.rb32
-rw-r--r--app/models/concerns/taskable.rb3
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb17
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encryption_helper.rb26
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb7
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb7
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/deployment.rb33
-rw-r--r--app/models/design_management/design_action.rb4
-rw-r--r--app/models/design_management/design_at_version.rb3
-rw-r--r--app/models/design_management/repository.rb2
-rw-r--r--app/models/design_management/version.rb4
-rw-r--r--app/models/environment.rb12
-rw-r--r--app/models/experiment.rb17
-rw-r--r--app/models/external_issue.rb3
-rw-r--r--app/models/gpg_key.rb4
-rw-r--r--app/models/group.rb62
-rw-r--r--app/models/internal_id.rb35
-rw-r--r--app/models/issue.rb24
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/list.rb1
-rw-r--r--app/models/member.rb15
-rw-r--r--app/models/members/group_member.rb4
-rw-r--r--app/models/members/last_group_owner_assigner.rb46
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb39
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/namespace.rb41
-rw-r--r--app/models/namespace/admin_note.rb7
-rw-r--r--app/models/namespace/traversal_hierarchy.rb26
-rw-r--r--app/models/namespace_setting.rb14
-rw-r--r--app/models/namespaces/traversal/linear.rb93
-rw-r--r--app/models/namespaces/traversal/recursive.rb15
-rw-r--r--app/models/note.rb19
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/packages/debian/file_entry.rb44
-rw-r--r--app/models/packages/debian/file_metadatum.rb2
-rw-r--r--app/models/packages/dependency.rb4
-rw-r--r--app/models/packages/go/module_version.rb4
-rw-r--r--app/models/packages/maven/metadatum.rb1
-rw-r--r--app/models/packages/package.rb26
-rw-r--r--app/models/packages/tag.rb2
-rw-r--r--app/models/pages/lookup_path.rb6
-rw-r--r--app/models/pages_deployment.rb2
-rw-r--r--app/models/preloaders/labels_preloader.rb34
-rw-r--r--app/models/preloaders/user_max_access_level_in_projects_preloader.rb25
-rw-r--r--app/models/project.rb69
-rw-r--r--app/models/project_feature.rb3
-rw-r--r--app/models/project_feature_usage.rb25
-rw-r--r--app/models/project_services/asana_service.rb24
-rw-r--r--app/models/project_services/assembla_service.rb2
-rw-r--r--app/models/project_services/bamboo_service.rb41
-rw-r--r--app/models/project_services/chat_message/merge_message.rb2
-rw-r--r--app/models/project_services/chat_notification_service.rb8
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb6
-rw-r--r--app/models/project_services/datadog_service.rb4
-rw-r--r--app/models/project_services/discord_service.rb18
-rw-r--r--app/models/project_services/drone_ci_service.rb12
-rw-r--r--app/models/project_services/emails_on_push_service.rb30
-rw-r--r--app/models/project_services/external_wiki_service.rb16
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jenkins_service.rb40
-rw-r--r--app/models/project_services/jira_service.rb112
-rw-r--r--app/models/project_services/jira_tracker_data.rb21
-rw-r--r--app/models/project_services/mattermost_service.rb19
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb2
-rw-r--r--app/models/project_services/microsoft_teams_service.rb14
-rw-r--r--app/models/project_services/mock_ci_service.rb7
-rw-r--r--app/models/project_services/pipelines_email_service.rb8
-rw-r--r--app/models/project_services/pushover_service.rb2
-rw-r--r--app/models/project_services/redmine_service.rb8
-rw-r--r--app/models/project_services/slack_service.rb4
-rw-r--r--app/models/project_services/teamcity_service.rb38
-rw-r--r--app/models/project_services/youtrack_service.rb4
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/raw_usage_data.rb4
-rw-r--r--app/models/release.rb2
-rw-r--r--app/models/release_highlight.rb23
-rw-r--r--app/models/remote_mirror.rb39
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/sent_notification.rb2
-rw-r--r--app/models/service.rb12
-rw-r--r--app/models/sidebars/context.rb21
-rw-r--r--app/models/sidebars/menu.rb82
-rw-r--r--app/models/sidebars/menu_item.rb21
-rw-r--r--app/models/sidebars/panel.rb75
-rw-r--r--app/models/sidebars/projects/context.rb11
-rw-r--r--app/models/sidebars/projects/menus/learn_gitlab/menu.rb41
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu.rb45
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb35
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/details.rb36
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb40
-rw-r--r--app/models/sidebars/projects/menus/repository/menu.rb59
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/branches.rb35
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/commits.rb35
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/compare.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/contributors.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/files.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/graphs.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/tags.rb28
-rw-r--r--app/models/sidebars/projects/menus/scope/menu.rb21
-rw-r--r--app/models/sidebars/projects/panel.rb26
-rw-r--r--app/models/timelog.rb4
-rw-r--r--app/models/todo.rb18
-rw-r--r--app/models/user.rb76
-rw-r--r--app/models/user_callout.rb10
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/models/users/in_product_marketing_email.rb49
-rw-r--r--app/models/users/merge_request_interaction.rb44
-rw-r--r--app/models/wiki.rb59
182 files changed, 3019 insertions, 676 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 514e923c380..ba46a98b951 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -58,7 +58,8 @@ class Ability
def allowed?(user, action, subject = :global, opts = {})
if subject.is_a?(Hash)
- opts, subject = subject, :global
+ opts = subject
+ subject = :global
end
policy = policy_for(user, subject)
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 44d1b6cf907..1bbace791ed 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -42,10 +42,6 @@ class ApplicationRecord < ActiveRecord::Base
false
end
- def self.at_most(count)
- limit(count)
- end
-
def self.safe_find_or_create_by!(*args, &block)
safe_find_or_create_by(*args, &block).tap do |record|
raise ActiveRecord::RecordNotFound unless record.present?
@@ -56,9 +52,9 @@ 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_statement_timeout
+ def self.with_fast_read_statement_timeout(timeout_ms = 5000)
transaction(requires_new: true) do
- connection.exec_query("SET LOCAL statement_timeout = 5000")
+ connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
yield
end
@@ -83,3 +79,5 @@ class ApplicationRecord < ActiveRecord::Base
enum(enum_mod.key => values)
end
end
+
+ApplicationRecord.prepend_if_ee('EE::ApplicationRecordHelpers')
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 44eb2fefb3f..f405f5ca5d3 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -465,6 +465,16 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
+ validates :admin_mode,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+
+ validates :external_pipeline_validation_service_url,
+ addressable_url: true, allow_blank: true
+
+ validates :external_pipeline_validation_service_timeout,
+ allow_nil: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -493,6 +503,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm
+ attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_truncated_aes_256_gcm
validates :disable_feed_token,
inclusion: { in: [true, false], message: _('must be a boolean value') }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index c067199b52c..66a8d1f8105 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -35,6 +35,7 @@ module ApplicationSettingImplementation
class_methods do
def defaults
{
+ admin_mode: false,
after_sign_up_text: nil,
akismet_enabled: false,
allow_local_requests_from_system_hooks: true,
@@ -71,6 +72,9 @@ module ApplicationSettingImplementation
eks_secret_access_key: nil,
email_restrictions_enabled: false,
email_restrictions: nil,
+ external_pipeline_validation_service_timeout: nil,
+ external_pipeline_validation_service_token: nil,
+ external_pipeline_validation_service_url: nil,
first_day_of_week: 0,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
@@ -434,11 +438,14 @@ module ApplicationSettingImplementation
def parse_addr_and_port(str)
case str
when /\A\[(?<address> .* )\]:(?<port> \d+ )\z/x # string like "[::1]:80"
- address, port = $~[:address], $~[:port]
+ address = $~[:address]
+ port = $~[:port]
when /\A(?<address> [^:]+ ):(?<port> \d+ )\z/x # string like "127.0.0.1:80"
- address, port = $~[:address], $~[:port]
+ address = $~[:address]
+ port = $~[:port]
else # string with no port number
- address, port = str, nil
+ address = str
+ port = nil
end
[address, port&.to_i]
diff --git a/app/models/audit_event_archived.rb b/app/models/audit_event_archived.rb
deleted file mode 100644
index 3119f56fbcc..00000000000
--- a/app/models/audit_event_archived.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-# This model is not intended to be used.
-# It is a temporary reference to the pre-partitioned
-# audit_events table.
-# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206
-# for details.
-class AuditEventArchived < ApplicationRecord
- self.table_name = 'audit_events_archived'
-end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 8a9db8b45ea..2185233a1ac 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -2,6 +2,7 @@
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
class Blob < SimpleDelegator
+ include GlobalID::Identification
include Presentable
include BlobLanguageFromGitAttributes
include BlobActiveModel
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index 1be7120a955..a851f22bfcd 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -33,8 +33,8 @@ module BlobViewer
@json_data ||= begin
prepare!
Gitlab::Json.parse(blob.data)
- rescue
- {}
+ rescue
+ {}
end
end
diff --git a/app/models/board.rb b/app/models/board.rb
index 418ea67fc6a..b26a9461ffc 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -34,14 +34,6 @@ class Board < ApplicationRecord
project_id.present?
end
- def backlog_list
- lists.merge(List.backlog).take
- end
-
- def closed_list
- lists.merge(List.closed).take
- end
-
def scoped?
false
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 9127dab56a6..04af1145769 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -68,25 +68,6 @@ class BulkImports::Entity < ApplicationRecord
end
end
- def update_tracker_for(relation:, has_next_page:, next_page: nil)
- attributes = {
- relation: relation,
- has_next_page: has_next_page,
- next_page: next_page,
- bulk_import_entity_id: id
- }
-
- trackers.upsert(attributes, unique_by: %i[bulk_import_entity_id relation])
- end
-
- def has_next_page?(relation)
- trackers.find_by(relation: relation)&.has_next_page
- end
-
- def next_page_for(relation)
- trackers.find_by(relation: relation)&.next_page
- end
-
private
def validate_parent_is_a_group
diff --git a/app/models/bulk_imports/stage.rb b/app/models/bulk_imports/stage.rb
new file mode 100644
index 00000000000..050c2c76ce8
--- /dev/null
+++ b/app/models/bulk_imports/stage.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class Stage
+ include Singleton
+
+ CONFIG = {
+ group: {
+ pipeline: BulkImports::Groups::Pipelines::GroupPipeline,
+ stage: 0
+ },
+ subgroups: {
+ pipeline: BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
+ stage: 1
+ },
+ members: {
+ pipeline: BulkImports::Groups::Pipelines::MembersPipeline,
+ stage: 1
+ },
+ labels: {
+ pipeline: BulkImports::Groups::Pipelines::LabelsPipeline,
+ stage: 1
+ },
+ milestones: {
+ pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline,
+ stage: 1
+ },
+ badges: {
+ pipeline: BulkImports::Groups::Pipelines::BadgesPipeline,
+ stage: 1
+ },
+ finisher: {
+ pipeline: BulkImports::Groups::Pipelines::EntityFinisher,
+ stage: 2
+ }
+ }.freeze
+
+ def self.pipelines
+ instance.pipelines
+ end
+
+ def self.pipeline_exists?(name)
+ pipelines.any? do |(_, pipeline)|
+ pipeline.to_s == name.to_s
+ end
+ end
+
+ def pipelines
+ @pipelines ||= config
+ .values
+ .sort_by { |entry| entry[:stage] }
+ .map do |entry|
+ [entry[:stage], entry[:pipeline]]
+ end
+ end
+
+ private
+
+ def config
+ @config ||= CONFIG
+ end
+ end
+end
+
+::BulkImports::Stage.prepend_if_ee('::EE::BulkImports::Stage')
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 182c0bbaa8a..282ba9e19ac 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -3,6 +3,8 @@
class BulkImports::Tracker < ApplicationRecord
self.table_name = 'bulk_import_trackers'
+ alias_attribute :pipeline_name, :relation
+
belongs_to :entity,
class_name: 'BulkImports::Entity',
foreign_key: :bulk_import_entity_id,
@@ -16,6 +18,29 @@ class BulkImports::Tracker < ApplicationRecord
validates :stage, presence: true
+ DEFAULT_PAGE_SIZE = 500
+
+ scope :next_pipeline_trackers_for, -> (entity_id) {
+ entity_scope = where(bulk_import_entity_id: entity_id)
+ next_stage_scope = entity_scope.with_status(:created).select('MIN(stage)')
+
+ entity_scope.where(stage: next_stage_scope)
+ }
+
+ def self.stage_running?(entity_id, stage)
+ where(stage: stage, bulk_import_entity_id: entity_id)
+ .with_status(:created, :started)
+ .exists?
+ end
+
+ def pipeline_class
+ unless BulkImports::Stage.pipeline_exists?(pipeline_name)
+ raise NameError.new("'#{pipeline_name}' is not a valid BulkImport Pipeline")
+ end
+
+ pipeline_name.constantize
+ end
+
state_machine :status, initial: :created do
state :created, value: 0
state :started, value: 1
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 824e35a6480..3d8e9f4c126 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -14,8 +14,6 @@ module Ci
BuildArchivedError = Class.new(StandardError)
- ignore_columns :artifacts_file, :artifacts_file_store, :artifacts_metadata, :artifacts_metadata_store, :artifacts_size, :commands, remove_after: '2019-12-15', remove_with: '12.7'
-
belongs_to :project, inverse_of: :builds
belongs_to :runner
belongs_to :trigger_request
@@ -35,6 +33,7 @@ module Ci
}.freeze
DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
+ RUNNERS_STATUS_CACHE_EXPIRATION = 1.minute
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
@@ -75,7 +74,14 @@ module Ci
return unless has_environment?
strong_memoize(:persisted_environment) do
- Environment.find_by(name: expanded_environment_name, project: project)
+ # This code path has caused N+1s in the past, since environments are only indirectly
+ # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
+ # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
+ BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
+ Environment.where(name: names, project: args[:key]).find_each do |environment|
+ loader.call(environment.name, environment)
+ end
+ end
end
end
@@ -88,8 +94,7 @@ module Ci
validates :ref, presence: true
scope :not_interruptible, -> do
- joins(:metadata).where('ci_builds_metadata.id NOT IN (?)',
- Ci::BuildMetadata.scoped_build.with_interruptible.select(:id))
+ joins(:metadata).where.not('ci_builds_metadata.id' => Ci::BuildMetadata.scoped_build.with_interruptible.select(:id))
end
scope :unstarted, -> { where(runner_id: nil) }
@@ -319,7 +324,7 @@ module Ci
end
end
- before_transition any => [:failed] do |build|
+ after_transition any => [:failed] do |build|
next unless build.project
next unless build.deployment
@@ -372,11 +377,11 @@ module Ci
end
def other_manual_actions
- pipeline.manual_actions.where.not(name: name)
+ pipeline.manual_actions.reject { |action| action.name == self.name }
end
def other_scheduled_actions
- pipeline.scheduled_actions.where.not(name: name)
+ pipeline.scheduled_actions.reject { |action| action.name == self.name }
end
def pages_generator?
@@ -698,7 +703,23 @@ module Ci
end
def any_runners_online?
- project.any_active_runners? { |runner| runner.match_build_if_online?(self) }
+ 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) }
+ 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?
+ end
end
def stuck?
@@ -1103,6 +1124,20 @@ module Ci
.to_a
.include?(exit_code)
end
+
+ def cache_for_online_runners(&block)
+ Rails.cache.fetch(
+ ['has-online-runners', id],
+ expires_in: RUNNERS_STATUS_CACHE_EXPIRATION
+ ) { yield }
+ end
+
+ def cache_for_available_runners(&block)
+ Rails.cache.fetch(
+ ['has-available-runners', project.id],
+ expires_in: RUNNERS_STATUS_CACHE_EXPIRATION
+ ) { yield }
+ end
end
end
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index b50ecf99439..8ae921f1416 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -21,8 +21,7 @@ module Ci
deps = model_class.where(pipeline_id: processable.pipeline_id).latest
deps = from_previous_stages(deps)
deps = from_needs(deps)
- deps = from_dependencies(deps)
- deps
+ from_dependencies(deps)
end
# Dependencies from the same parent-pipeline hierarchy excluding
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index d4f9f78a1ac..7e03d709f24 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -30,9 +30,9 @@ module Ci
fog: 3
}.freeze
- STORE_TYPES = DATA_STORES.keys.map do |store|
+ STORE_TYPES = DATA_STORES.keys.to_h do |store|
[store, "Ci::BuildTraceChunks::#{store.capitalize}".constantize]
- end.to_h.freeze
+ end.freeze
enum data_store: DATA_STORES
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
index 58d50b39c11..003ec107895 100644
--- a/app/models/ci/build_trace_chunks/redis.rb
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -4,7 +4,7 @@ module Ci
module BuildTraceChunks
class Redis
CHUNK_REDIS_TTL = 1.week
- LUA_APPEND_CHUNK = <<~EOS.freeze
+ 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}
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 4ba09fd8152..47b91fcf2ce 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -22,6 +22,13 @@ module Ci
@jobs = jobs
end
+ def ==(other)
+ other.present? && other.is_a?(self.class) &&
+ project == other.project &&
+ stage == other.stage &&
+ name == other.name
+ end
+
def status
strong_memoize(:status) do
status_struct.status
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index d5e88f2be5b..50e21a1c323 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -131,8 +131,6 @@ module Ci
update_project_statistics project_statistics_name: :build_artifacts_size
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
- scope :with_files_stored_locally, -> { where(file_store: ::JobArtifactUploader::Store::LOCAL) }
- scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
@@ -292,8 +290,12 @@ module Ci
end
end
+ def archived_trace_exists?
+ file&.file&.exists?
+ end
+
def self.archived_trace_exists_for?(job_id)
- where(job_id: job_id).trace.take&.file&.file&.exists?
+ where(job_id: job_id).trace.take&.archived_trace_exists?
end
def self.max_artifact_size(type:, project:)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index b63ec0c8a97..c9ab69317e1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -286,9 +286,11 @@ module Ci
end
after_transition any => [:failed] do |pipeline|
- next unless pipeline.auto_devops_source?
+ pipeline.run_after_commit do
+ ::Gitlab::Ci::Pipeline::Metrics.pipeline_failure_reason_counter.increment(reason: pipeline.failure_reason)
- pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
+ AutoDevops::DisableWorker.perform_async(pipeline.id) if pipeline.auto_devops_source?
+ end
end
end
@@ -309,6 +311,7 @@ module Ci
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
+ scope :eager_load_project, -> { eager_load(project: [:route, { namespace: :route }]) }
scope :outside_pipeline_family, ->(pipeline) do
where.not(id: pipeline.same_family_pipeline_ids)
@@ -393,26 +396,13 @@ module Ci
# given we simply get the latest pipelines for the commits, regardless
# of what refs the pipelines belong to.
def self.latest_pipeline_per_commit(commits, ref = nil)
- p1 = arel_table
- p2 = arel_table.alias
-
- # This LEFT JOIN will filter out all but the newest row for every
- # combination of (project_id, sha) or (project_id, sha, ref) if a ref is
- # given.
- cond = p1[:sha].eq(p2[:sha])
- .and(p1[:project_id].eq(p2[:project_id]))
- .and(p1[:id].lt(p2[:id]))
-
- cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
- join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
+ sql = select('DISTINCT ON (sha) *')
+ .where(sha: commits)
+ .order(:sha, id: :desc)
- relation = where(sha: commits)
- .where(p2[:id].eq(nil))
- .joins(join.join_sources)
+ sql = sql.where(ref: ref) if ref
- relation = relation.where(ref: ref) if ref
-
- relation.each_with_object({}) do |pipeline, hash|
+ sql.each_with_object({}) do |pipeline, hash|
hash[pipeline.sha] = pipeline
end
end
@@ -445,6 +435,10 @@ module Ci
@auto_devops_pipelines_completed_total ||= Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines')
end
+ def uses_needs?
+ builds.where(scheduling_type: :dag).any?
+ end
+
def stages_count
statuses.select(:stage).distinct.count
end
@@ -510,6 +504,12 @@ module Ci
end
end
+ def git_author_full_text
+ strong_memoize(:git_author_full_text) do
+ commit.try(:author_full_text)
+ end
+ end
+
def git_commit_message
strong_memoize(:git_commit_message) do
commit.try(:message)
@@ -573,10 +573,18 @@ module Ci
end
def cancel_running(retries: nil)
- retry_optimistic_lock(cancelable_statuses, retries, name: 'ci_pipeline_cancel_running') do |cancelable|
- cancelable.find_each do |job|
- yield(job) if block_given?
- job.cancel
+ commit_status_relations = [:project, :pipeline]
+ ci_build_relations = [:deployment, :taggings]
+
+ retry_optimistic_lock(cancelable_statuses, retries, name: 'ci_pipeline_cancel_running') do |cancelables|
+ cancelables.find_in_batches do |batch|
+ ActiveRecord::Associations::Preloader.new.preload(batch, commit_status_relations)
+ ActiveRecord::Associations::Preloader.new.preload(batch.select { |job| job.is_a?(Ci::Build) }, ci_build_relations)
+
+ batch.each do |job|
+ yield(job) if block_given?
+ job.cancel
+ end
end
end
end
@@ -664,7 +672,9 @@ module Ci
end
def has_kubernetes_active?
- project.deployment_platform&.active?
+ strong_memoize(:has_kubernetes_active) do
+ project.deployment_platform&.active?
+ end
end
def freeze_period?
@@ -822,6 +832,7 @@ module Ci
variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)
+ variables.append(key: 'CI_COMMIT_AUTHOR', value: git_author_full_text.to_s)
# legacy variables
variables.append(key: 'CI_BUILD_REF', value: sha)
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index f538a4cd808..9dfe4252e95 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -57,3 +57,5 @@ module Ci
end
end
end
+
+Ci::PipelineArtifact.prepend_ee_mod
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 2fae077dd87..3c17246bc34 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -7,6 +7,7 @@ module Ci
include StripAttribute
include Schedulable
include Limitable
+ include EachBatch
self.limit_name = 'ci_pipeline_schedules'
self.limit_scope = :project
@@ -28,6 +29,7 @@ module Ci
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :preloaded, -> { preload(:owner, project: [:route]) }
+ scope :owned_by, ->(user) { where(owner: user) }
accepts_nested_attributes_for :variables, allow_destroy: true
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 0ad1ed2fce8..3b61840805a 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -165,7 +165,13 @@ module Ci
end
def all_dependencies
- dependencies.all
+ if Feature.enabled?(:preload_associations_jobs_request_api_endpoint, project, default_enabled: :yaml)
+ strong_memoize(:all_dependencies) do
+ dependencies.all
+ end
+ else
+ dependencies.all
+ end
end
private
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index d1a20bc93c3..05126853e0f 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -45,8 +45,6 @@ module Ci
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
MINUTES_COST_FACTOR_FIELDS = %i[public_projects_minutes_cost_factor private_projects_minutes_cost_factor].freeze
- ignore_column :is_shared, remove_after: '2019-12-15', remove_with: '12.6'
-
has_many :builds
has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 03a97355574..9dd75150ac7 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -14,11 +14,20 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :latest_statuses, -> { ordered.latest }, class_name: 'CommitStatus', foreign_key: :stage_id
+ has_many :retried_statuses, -> { ordered.retried }, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :processables, class_name: 'Ci::Processable', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
has_many :bridges, foreign_key: :stage_id
scope :ordered, -> { order(position: :asc) }
+ scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
+ scope :by_name, ->(names) { where(name: names) }
+ scope :with_latest_and_retried_statuses, -> do
+ includes(
+ latest_statuses: [:pipeline, project: :namespace],
+ retried_statuses: [:pipeline, project: :namespace]
+ )
+ end
with_options unless: :importing? do
validates :project, presence: true
@@ -35,7 +44,7 @@ module Ci
next if position.present?
self.position = statuses.select(:stage_idx)
- .where('stage_idx IS NOT NULL')
+ .where.not(stage_idx: nil)
.group(:stage_idx)
.order('COUNT(*) DESC')
.first&.stage_idx.to_i
diff --git a/app/models/ci/test_case.rb b/app/models/ci/test_case.rb
deleted file mode 100644
index 19ecc177436..00000000000
--- a/app/models/ci/test_case.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class TestCase < ApplicationRecord
- extend Gitlab::Ci::Model
-
- validates :project, :key_hash, presence: true
-
- has_many :test_case_failures, class_name: 'Ci::TestCaseFailure'
-
- belongs_to :project
-
- scope :by_project_and_keys, -> (project, keys) { where(project_id: project.id, key_hash: keys) }
-
- class << self
- def find_or_create_by_batch(project, test_case_keys)
- # Insert records first. Existing ones will be skipped.
- insert_all(test_case_attrs(project, test_case_keys))
-
- # Find all matching records now that we are sure they all are persisted.
- by_project_and_keys(project, test_case_keys)
- end
-
- private
-
- def test_case_attrs(project, test_case_keys)
- # NOTE: Rails 6.1 will add support for insert_all on relation so that
- # we will be able to do project.test_cases.insert_all.
- test_case_keys.map do |hashed_key|
- { project_id: project.id, key_hash: hashed_key }
- end
- end
- end
- end
-end
diff --git a/app/models/ci/test_case_failure.rb b/app/models/ci/test_case_failure.rb
deleted file mode 100644
index 8867b954240..00000000000
--- a/app/models/ci/test_case_failure.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class TestCaseFailure < ApplicationRecord
- extend Gitlab::Ci::Model
-
- REPORT_WINDOW = 14.days
-
- validates :test_case, :build, :failed_at, presence: true
-
- belongs_to :test_case, class_name: "Ci::TestCase", foreign_key: :test_case_id
- belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
-
- def self.recent_failures_count(project:, test_case_keys:, date_range: REPORT_WINDOW.ago..Time.current)
- joins(:test_case)
- .where(
- ci_test_cases: {
- project_id: project.id,
- key_hash: test_case_keys
- },
- ci_test_case_failures: {
- failed_at: date_range
- }
- )
- .group(:key_hash)
- .count('ci_test_case_failures.id')
- end
- end
-end
diff --git a/app/models/ci/unit_test.rb b/app/models/ci/unit_test.rb
new file mode 100644
index 00000000000..81623b4f6ad
--- /dev/null
+++ b/app/models/ci/unit_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Ci
+ class UnitTest < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ MAX_NAME_SIZE = 255
+ MAX_SUITE_NAME_SIZE = 255
+
+ validates :project, :key_hash, :name, :suite_name, presence: true
+
+ has_many :unit_test_failures, class_name: 'Ci::UnitTestFailure'
+
+ belongs_to :project
+
+ scope :by_project_and_keys, -> (project, keys) { where(project_id: project.id, key_hash: keys) }
+
+ class << self
+ def find_or_create_by_batch(project, unit_test_attrs)
+ # Insert records first. Existing ones will be skipped.
+ insert_all(build_insert_attrs(project, unit_test_attrs))
+
+ # Find all matching records now that we are sure they all are persisted.
+ by_project_and_keys(project, gather_keys(unit_test_attrs))
+ end
+
+ private
+
+ def build_insert_attrs(project, unit_test_attrs)
+ # NOTE: Rails 6.1 will add support for insert_all on relation so that
+ # we will be able to do project.test_cases.insert_all.
+ unit_test_attrs.map do |attrs|
+ attrs.merge(
+ project_id: project.id,
+ name: attrs[:name].truncate(MAX_NAME_SIZE),
+ suite_name: attrs[:suite_name].truncate(MAX_SUITE_NAME_SIZE)
+ )
+ end
+ end
+
+ def gather_keys(unit_test_attrs)
+ unit_test_attrs.map { |attrs| attrs[:key_hash] }
+ end
+ end
+ end
+end
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
new file mode 100644
index 00000000000..653a56bd2b3
--- /dev/null
+++ b/app/models/ci/unit_test_failure.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Ci
+ class UnitTestFailure < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ REPORT_WINDOW = 14.days
+
+ validates :unit_test, :build, :failed_at, presence: true
+
+ belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id
+ belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+
+ def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current)
+ joins(:unit_test)
+ .where(
+ ci_unit_tests: {
+ project_id: project.id,
+ key_hash: unit_test_keys
+ },
+ ci_unit_test_failures: {
+ failed_at: date_range
+ }
+ )
+ .group(:key_hash)
+ .count('ci_unit_test_failures.id')
+ end
+ end
+end
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index 9d79887b574..d42279502c5 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -2,17 +2,45 @@
module Clusters
class AgentToken < ApplicationRecord
+ include RedisCacheable
include TokenAuthenticatable
+
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
+ cached_attr_reader :last_contacted_at
self.table_name = 'cluster_agent_tokens'
+ # The `UPDATE_USED_COLUMN_EVERY` defines how often the token DB entry can be updated
+ UPDATE_USED_COLUMN_EVERY = (40.minutes..55.minutes).freeze
+
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
belongs_to :created_by_user, class_name: 'User', optional: true
before_save :ensure_token
validates :description, length: { maximum: 1024 }
- validates :name, presence: true, length: { maximum: 255 }, on: :create
+ validates :name, presence: true, length: { maximum: 255 }
+
+ def track_usage
+ track_values = { last_used_at: Time.current.utc }
+
+ cache_attributes(track_values)
+
+ # Use update_column so updated_at is skipped
+ update_columns(track_values) if can_update_track_values?
+ end
+
+ private
+
+ def can_update_track_values?
+ # Use a random threshold to prevent beating DB updates.
+ last_used_at_max_age = Random.rand(UPDATE_USED_COLUMN_EVERY)
+
+ real_last_used_at = read_attribute(:last_used_at)
+
+ # Handle too many updates from high token traffic
+ real_last_used_at.nil? ||
+ (Time.current - real_last_used_at) >= last_used_at_max_age
+ end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 55a9a0ccb81..b9c136abab4 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Prometheus < ApplicationRecord
- include PrometheusAdapter
+ include ::Clusters::Concerns::PrometheusClient
VERSION = '10.4.1'
@@ -32,7 +32,7 @@ module Clusters
end
state_machine :status do
- after_transition any => [:installed] do |application|
+ after_transition any => [:installed, :externally_installed] do |application|
application.run_after_commit do
Clusters::Applications::ActivateServiceWorker
.perform_async(application.cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
@@ -58,14 +58,6 @@ module Clusters
'https://gitlab-org.gitlab.io/cluster-integration/helm-stable-archive'
end
- def service_name
- 'prometheus-prometheus-server'
- end
-
- def service_port
- 80
- end
-
def install_command
helm_command_module::InstallCommand.new(
name: name,
@@ -106,29 +98,6 @@ module Clusters
files.merge('values.yaml': replaced_values)
end
- def prometheus_client
- return unless kube_client
-
- proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE)
-
- # ensures headers containing auth data are appended to original k8s client options
- options = kube_client.rest_client.options
- .merge(prometheus_client_default_options)
- .merge(headers: kube_client.headers)
- Gitlab::PrometheusClient.new(proxy_url, options)
- rescue Kubeclient::HttpError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENETUNREACH
- # If users have mistakenly set parameters or removed the depended clusters,
- # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
- # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab,
- # we need to silence the exceptions
- end
-
- def configured?
- kube_client.present? && available?
- rescue Gitlab::UrlBlocker::BlockedUrlError
- false
- end
-
def generate_alert_manager_token!
unless alert_manager_token.present?
update!(alert_manager_token: generate_token)
@@ -146,10 +115,6 @@ module Clusters
.perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
end
- def kube_client
- cluster&.kubeclient&.core_client
- end
-
def install_knative_metrics
return [] unless cluster.application_knative_available?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 8a49d476ba7..bc80bcd0b06 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.26.0'
+ VERSION = '0.27.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index a34d8a6b98d..a1e2aa194a0 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -51,6 +51,8 @@ module Clusters
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
+ has_one :integration_prometheus, class_name: 'Clusters::Integrations::Prometheus', inverse_of: :cluster
+
def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateName
application = APPLICATIONS[name.to_s]
has_one application.association_name, class_name: application.to_s, inverse_of: :cluster # rubocop:disable Rails/ReflectionClassName
@@ -100,7 +102,6 @@ module Clusters
delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
- delegate :available?, to: :application_prometheus, 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 :external_ip, to: :application_ingress, prefix: true, allow_nil: true
@@ -148,6 +149,9 @@ module Clusters
scope :with_management_project, -> { where.not(management_project: nil) }
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
+
+ # with_application_prometheus scope is deprecated, and scheduled for removal
+ # in %14.0. See https://gitlab.com/groups/gitlab-org/-/epics/4280
scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) }
scope :with_project_http_integrations, -> (project_ids) do
conditions = { projects: :alert_management_http_integrations }
@@ -276,6 +280,10 @@ module Clusters
public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend
end
+ def find_or_build_integration_prometheus
+ integration_prometheus || build_integration_prometheus
+ end
+
def provider
if gcp?
provider_gcp
@@ -361,8 +369,12 @@ module Clusters
end
end
+ def application_prometheus_available?
+ integration_prometheus&.available? || application_prometheus&.available?
+ end
+
def prometheus_adapter
- application_prometheus
+ integration_prometheus || application_prometheus
end
private
diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb
index c9c18d8c96a..125783e6ee1 100644
--- a/app/models/clusters/clusters_hierarchy.rb
+++ b/app/models/clusters/clusters_hierarchy.rb
@@ -16,7 +16,7 @@ module Clusters
model
.unscoped
- .where('clusters.id IS NOT NULL')
+ .where.not('clusters.id' => nil)
.with
.recursive(cte.to_arel)
.from(cte_alias)
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 95ac95448dd..7485ee079ce 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -9,6 +9,7 @@ module Clusters
scope :available, -> do
where(
status: [
+ self.state_machines[:status].states[:externally_installed].value,
self.state_machines[:status].states[:installed].value,
self.state_machines[:status].states[:updated].value
]
@@ -28,6 +29,7 @@ module Clusters
state :uninstalling, value: 7
state :uninstall_errored, value: 8
state :uninstalled, value: 10
+ state :externally_installed, value: 11
# Used for applications that are pre-installed by the cluster,
# e.g. Knative in GCP Cloud Run enabled clusters
@@ -37,7 +39,7 @@ module Clusters
state :pre_installed, value: 9
event :make_externally_installed do
- transition any => :installed
+ transition any => :externally_installed
end
event :make_externally_uninstalled do
@@ -79,7 +81,7 @@ module Clusters
transition [:scheduled] => :uninstalling
end
- before_transition any => [:scheduled, :installed, :uninstalled] do |application, _|
+ before_transition any => [:scheduled, :installed, :uninstalled, :externally_installed] do |application, _|
application.status_reason = nil
end
@@ -114,7 +116,7 @@ module Clusters
end
def available?
- pre_installed? || installed? || updated?
+ pre_installed? || installed? || externally_installed? || updated?
end
def update_in_progress?
diff --git a/app/models/clusters/concerns/application_version.rb b/app/models/clusters/concerns/application_version.rb
index 6c0b014662c..dab0bd23e2e 100644
--- a/app/models/clusters/concerns/application_version.rb
+++ b/app/models/clusters/concerns/application_version.rb
@@ -5,11 +5,17 @@ module Clusters
module ApplicationVersion
extend ActiveSupport::Concern
+ EXTERNAL_VERSION = 'EXTERNALLY_INSTALLED'
+
included do
state_machine :status do
before_transition any => [:installed, :updated] do |application|
application.version = application.class.const_get(:VERSION, false)
end
+
+ before_transition any => [:externally_installed] do |application|
+ application.version = EXTERNAL_VERSION
+ end
end
end
diff --git a/app/models/clusters/concerns/prometheus_client.rb b/app/models/clusters/concerns/prometheus_client.rb
new file mode 100644
index 00000000000..10cb307addd
--- /dev/null
+++ b/app/models/clusters/concerns/prometheus_client.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Concerns
+ module PrometheusClient
+ extend ActiveSupport::Concern
+
+ included do
+ include PrometheusAdapter
+
+ def service_name
+ 'prometheus-prometheus-server'
+ end
+
+ def service_port
+ 80
+ end
+
+ def prometheus_client
+ return unless kube_client
+
+ proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE)
+
+ # ensures headers containing auth data are appended to original k8s client options
+ options = kube_client.rest_client.options
+ .merge(prometheus_client_default_options)
+ .merge(headers: kube_client.headers)
+ Gitlab::PrometheusClient.new(proxy_url, options)
+ rescue Kubeclient::HttpError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENETUNREACH
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab,
+ # we need to silence the exceptions
+ end
+
+ def configured?
+ kube_client.present? && available?
+ rescue Gitlab::UrlBlocker::BlockedUrlError
+ false
+ end
+
+ private
+
+ def kube_client
+ cluster&.kubeclient&.core_client
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
new file mode 100644
index 00000000000..1496d8ff1dd
--- /dev/null
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Integrations
+ class Prometheus < ApplicationRecord
+ include ::Clusters::Concerns::PrometheusClient
+
+ self.table_name = 'clusters_integration_prometheus'
+ self.primary_key = :cluster_id
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ validates :cluster, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+
+ def available?
+ enabled?
+ end
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index bf168aaacc5..5c3e3685c64 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -62,7 +62,8 @@ class Commit
collection.sort do |a, b|
operands = [a, b].tap { |o| o.reverse! if sort == 'desc' }
- attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend
+ attr1 = operands.first.public_send(order_by) # rubocop:disable GitlabSecurity/PublicSend
+ attr2 = operands.second.public_send(order_by) # rubocop:disable GitlabSecurity/PublicSend
# use case insensitive comparison for string values
order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2
@@ -222,6 +223,14 @@ class Commit
end
end
+ def author_full_text
+ return unless author_name && author_email
+
+ strong_memoize(:author_full_text) do
+ "#{author_name} <#{author_email}>"
+ end
+ end
+
# Returns full commit message if title is truncated (greater than 99 characters)
# otherwise returns commit message without first line
def description
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 524429bf12a..e989129209a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -55,6 +55,8 @@ class CommitStatus < ApplicationRecord
scope :for_ref, -> (ref) { where(ref: ref) }
scope :by_name, -> (name) { where(name: name) }
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
+ scope :eager_load_pipeline, -> { eager_load(:pipeline, project: { namespace: :route }) }
+ scope :with_pipeline, -> { joins(:pipeline) }
scope :for_project_paths, -> (paths) do
where(project: Project.where_full_path_in(Array(paths)))
@@ -179,14 +181,9 @@ class CommitStatus < ApplicationRecord
end
after_transition any => :failed do |commit_status|
- next unless commit_status.project
-
- # rubocop: disable CodeReuse/ServiceClass
commit_status.run_after_commit do
- MergeRequests::AddTodoWhenBuildFailsService
- .new(project, nil).execute(self)
+ ::Gitlab::Ci::Pipeline::Metrics.job_failure_reason_counter.increment(reason: commit_status.failure_reason)
end
- # rubocop: enable CodeReuse/ServiceClass
end
end
@@ -210,26 +207,7 @@ class CommitStatus < ApplicationRecord
end
def group_name
- simplified_commit_status_group_name_feature_flag = Gitlab::SafeRequestStore.fetch("project:#{project_id}:simplified_commit_status_group_name") do
- Feature.enabled?(:simplified_commit_status_group_name, project, default_enabled: false)
- end
-
- if simplified_commit_status_group_name_feature_flag
- # Only remove one or more [...] "X/Y" "X Y" from the end of build names.
- # More about the regular expression logic: https://docs.gitlab.com/ee/ci/jobs/#group-jobs-in-a-pipeline
-
- name.to_s.sub(%r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z}, '').strip
- else
- # Prior implementation, remove [...] "X/Y" "X Y" from the beginning and middle of build names
- # 'rspec:linux: 1/10' => 'rspec:linux'
- common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
-
- # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
- common_name.gsub!(%r{: \[.*\]\s*\z}, '')
-
- common_name.strip!
- common_name
- end
+ name.to_s.sub(%r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z}, '').strip
end
def failed_but_allowed?
@@ -293,7 +271,8 @@ class CommitStatus < ApplicationRecord
end
def update_older_statuses_retried!
- self.class
+ pipeline
+ .statuses
.latest
.where(name: name)
.where.not(id: id)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index c106c08c04a..fdc418029be 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -131,7 +131,6 @@ module Avatarable
def clear_avatar_caches
return unless respond_to?(:verified_emails) && verified_emails.any? && avatar_changed?
- return unless Feature.enabled?(:avatar_cache_for_email, self, type: :development)
Gitlab::AvatarCache.delete_by_email(*verified_emails)
end
diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb
index d6863e87261..b9827a79422 100644
--- a/app/models/concerns/boards/listable.rb
+++ b/app/models/concerns/boards/listable.rb
@@ -13,6 +13,7 @@ module Boards
scope :ordered, -> { order(:list_type, :position) }
scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
+ scope :without_types, ->(list_types) { where.not(list_type: list_types) }
class << self
def preload_preferences_for_user(lists, user)
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index f44ad474cd5..e252ca36629 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -13,13 +13,7 @@ module BulkMemberAccessLoad
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
- key = max_member_access_for_resource_key(resource_klass, memoization_index)
- access = {}
-
- if Gitlab::SafeRequestStore.active?
- Gitlab::SafeRequestStore[key] ||= {}
- access = Gitlab::SafeRequestStore[key]
- end
+ access = load_access_hash(resource_klass, memoization_index)
# Look up only the IDs we need
resource_ids -= access.keys
@@ -39,10 +33,28 @@ module BulkMemberAccessLoad
access
end
+ def merge_value_to_request_store(resource_klass, resource_id, memoization_index, value)
+ max_member_access_for_resource_ids(resource_klass, [resource_id], memoization_index) do
+ { resource_id => value }
+ end
+ end
+
private
def max_member_access_for_resource_key(klass, memoization_index)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
end
+
+ def load_access_hash(resource_klass, memoization_index)
+ key = max_member_access_for_resource_key(resource_klass, memoization_index)
+
+ access = {}
+ if Gitlab::SafeRequestStore.active?
+ Gitlab::SafeRequestStore[key] ||= {}
+ access = Gitlab::SafeRequestStore[key]
+ end
+
+ access
+ end
end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 45944401c2d..34c1b6d25a4 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -56,12 +56,12 @@ module CacheMarkdownField
# Update every applicable column in a row if any one is invalidated, as we only store
# one version per row
def refresh_markdown_cache
- updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
+ updates = cached_markdown_fields.markdown_fields.to_h do |markdown_field|
[
cached_markdown_fields.html_field(markdown_field),
rendered_field_content(markdown_field)
]
- end.to_h
+ end
updates['cached_markdown_version'] = latest_cached_markdown_version
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
new file mode 100644
index 00000000000..2b4a108a9a0
--- /dev/null
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+#
+# Cascading attributes enables managing settings in a flexible way.
+#
+# - Instance administrator can define an instance-wide default setting, or
+# lock the setting to prevent change by group owners.
+# - Group maintainers/owners can define a default setting for their group, or
+# lock the setting to prevent change by sub-group maintainers/owners.
+#
+# Behavior:
+#
+# - When a group does not have a value (value is `nil`), cascade up the
+# hierarchy to find the first non-nil value.
+# - Settings can be locked at any level to prevent groups/sub-groups from
+# overriding.
+# - If the setting isn't locked, the default can be overridden.
+# - An instance administrator or group maintainer/owner can push settings values
+# to groups/sub-groups to override existing values, even when the setting
+# is not otherwise locked.
+#
+module CascadingNamespaceSettingAttribute
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ class_methods do
+ def cascading_settings_feature_enabled?
+ ::Feature.enabled?(:cascading_namespace_settings, default_enabled: true)
+ end
+
+ private
+
+ # Facilitates the cascading lookup of values and,
+ # similar to Rails' `attr_accessor`, defines convenience methods such as
+ # a reader, writer, and validators.
+ #
+ # Example: `cascading_attr :delayed_project_removal`
+ #
+ # Public methods defined:
+ # - `delayed_project_removal`
+ # - `delayed_project_removal=`
+ # - `delayed_project_removal_locked?`
+ # - `delayed_project_removal_locked_by_ancestor?`
+ # - `delayed_project_removal_locked_by_application_setting?`
+ # - `delayed_project_removal?` (only defined for boolean attributes)
+ # - `delayed_project_removal_locked_ancestor` - Returns locked namespace settings object (only namespace_id)
+ #
+ # Defined validators ensure attribute value cannot be updated if locked by
+ # an ancestor or application settings.
+ #
+ # Requires database columns be present in both `namespace_settings` and
+ # `application_settings`.
+ def cascading_attr(*attributes)
+ attributes.map(&:to_sym).each do |attribute|
+ # public methods
+ define_attr_reader(attribute)
+ define_attr_writer(attribute)
+ define_lock_methods(attribute)
+ alias_boolean(attribute)
+
+ # private methods
+ define_validator_methods(attribute)
+ define_after_update(attribute)
+
+ validate :"#{attribute}_changeable?"
+ validate :"lock_#{attribute}_changeable?"
+
+ after_update :"clear_descendant_#{attribute}_locks", if: -> { saved_change_to_attribute?("lock_#{attribute}", to: true) }
+ end
+ end
+
+ # The cascading attribute reader method handles lookups
+ # with the following criteria:
+ #
+ # 1. Returns the dirty value, if the attribute has changed.
+ # 2. Return locked ancestor value.
+ # 3. Return locked instance-level application settings value.
+ # 4. Return this namespace's attribute, if not nil.
+ # 5. Return value from nearest ancestor where value is not nil.
+ # 6. Return instance-level application setting.
+ def define_attr_reader(attribute)
+ define_method(attribute) do
+ strong_memoize(attribute) do
+ next self[attribute] unless self.class.cascading_settings_feature_enabled?
+
+ next self[attribute] if will_save_change_to_attribute?(attribute)
+ next locked_value(attribute) if cascading_attribute_locked?(attribute)
+ next self[attribute] unless self[attribute].nil?
+
+ cascaded_value = cascaded_ancestor_value(attribute)
+ next cascaded_value unless cascaded_value.nil?
+
+ application_setting_value(attribute)
+ end
+ end
+ end
+
+ def define_attr_writer(attribute)
+ define_method("#{attribute}=") do |value|
+ clear_memoization(attribute)
+
+ super(value)
+ end
+ end
+
+ def define_lock_methods(attribute)
+ define_method("#{attribute}_locked?") do
+ cascading_attribute_locked?(attribute)
+ end
+
+ define_method("#{attribute}_locked_by_ancestor?") do
+ locked_by_ancestor?(attribute)
+ end
+
+ define_method("#{attribute}_locked_by_application_setting?") do
+ locked_by_application_setting?(attribute)
+ end
+
+ define_method("#{attribute}_locked_ancestor") do
+ locked_ancestor(attribute)
+ end
+ end
+
+ def alias_boolean(attribute)
+ return unless Gitlab::Database.exists? && type_for_attribute(attribute).type == :boolean
+
+ alias_method :"#{attribute}?", attribute
+ end
+
+ # Defines two validations - one for the cascadable attribute itself and one
+ # for the lock attribute. Only allows the respective value to change if
+ # an ancestor has not already locked the value.
+ def define_validator_methods(attribute)
+ define_method("#{attribute}_changeable?") do
+ return unless cascading_attribute_changed?(attribute)
+ return unless cascading_attribute_locked?(attribute)
+
+ errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
+ end
+
+ define_method("lock_#{attribute}_changeable?") do
+ return unless cascading_attribute_changed?("lock_#{attribute}")
+
+ if cascading_attribute_locked?(attribute)
+ return errors.add(:"lock_#{attribute}", s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
+ end
+
+ # Don't allow locking a `nil` attribute.
+ # Even if the value being locked is currently cascaded from an ancestor,
+ # it should be copied to this record to avoid the ancestor changing the
+ # value unexpectedly later.
+ return unless self[attribute].nil? && public_send("lock_#{attribute}?") # rubocop:disable GitlabSecurity/PublicSend
+
+ errors.add(attribute, s_('CascadingSettings|cannot be nil when locking the attribute'))
+ end
+
+ private :"#{attribute}_changeable?", :"lock_#{attribute}_changeable?"
+ end
+
+ # When a particular group locks the attribute, clear all sub-group locks
+ # since the higher lock takes priority.
+ def define_after_update(attribute)
+ define_method("clear_descendant_#{attribute}_locks") do
+ self.class.where(namespace_id: descendants).update_all("lock_#{attribute}" => false)
+ end
+
+ private :"clear_descendant_#{attribute}_locks"
+ end
+ end
+
+ private
+
+ def locked_value(attribute)
+ ancestor = locked_ancestor(attribute)
+ return ancestor.read_attribute(attribute) if ancestor
+
+ Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def locked_ancestor(attribute)
+ return unless self.class.cascading_settings_feature_enabled?
+ return unless namespace.has_parent?
+
+ strong_memoize(:"#{attribute}_locked_ancestor") do
+ self.class
+ .select(:namespace_id, "lock_#{attribute}", attribute)
+ .where(namespace_id: namespace_ancestor_ids)
+ .where(self.class.arel_table["lock_#{attribute}"].eq(true))
+ .limit(1).load.first
+ end
+ end
+
+ def locked_by_ancestor?(attribute)
+ return false unless self.class.cascading_settings_feature_enabled?
+
+ locked_ancestor(attribute).present?
+ end
+
+ def locked_by_application_setting?(attribute)
+ return false unless self.class.cascading_settings_feature_enabled?
+
+ Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def cascading_attribute_locked?(attribute)
+ locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
+ end
+
+ def cascading_attribute_changed?(attribute)
+ public_send("#{attribute}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def cascaded_ancestor_value(attribute)
+ return unless namespace.has_parent?
+
+ # rubocop:disable GitlabSecurity/SqlInjection
+ self.class
+ .select(attribute)
+ .joins("join unnest(ARRAY[#{namespace_ancestor_ids.join(',')}]) with ordinality t(namespace_id, ord) USING (namespace_id)")
+ .where("#{attribute} IS NOT NULL")
+ .order('t.ord')
+ .limit(1).first&.read_attribute(attribute)
+ # rubocop:enable GitlabSecurity/SqlInjection
+ end
+
+ def application_setting_value(attribute)
+ Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def namespace_ancestor_ids
+ strong_memoize(:namespace_ancestor_ids) do
+ namespace.self_and_ancestors(hierarchy_order: :asc).pluck(:id).reject { |id| id == namespace_id }
+ end
+ end
+
+ def descendants
+ strong_memoize(:descendants) do
+ namespace.descendants.pluck(:id)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index cbe7d3b6abb..0d29955268f 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -4,8 +4,10 @@ module Ci
module Artifactable
extend ActiveSupport::Concern
- NotSupportedAdapterError = Class.new(StandardError)
+ include ObjectStorable
+ STORE_COLUMN = :file_store
+ NotSupportedAdapterError = Class.new(StandardError)
FILE_FORMAT_ADAPTERS = {
gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream,
raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
@@ -20,6 +22,7 @@ module Ci
scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) }
scope :expired, -> (limit) { expired_before(Time.current).limit(limit) }
+ scope :project_id_in, ->(ids) { where(project_id: ids) }
end
def each_blob(&blk)
@@ -39,3 +42,5 @@ module Ci
end
end
end
+
+Ci::Artifactable.prepend_ee_mod
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 0412f7a072b..c990da5873a 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -16,6 +16,19 @@ module Ci
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
+ STATUSES_DESCRIPTION = {
+ created: 'Pipeline has been created',
+ waiting_for_resource: 'A resource (for example, a runner) that the pipeline requires to run is unavailable',
+ preparing: 'Pipeline is preparing to run',
+ pending: 'Pipeline has not started running yet',
+ running: 'Pipeline is running',
+ failed: 'At least one stage of the pipeline failed',
+ success: 'Pipeline completed successfully',
+ canceled: 'Pipeline was canceled before completion',
+ skipped: 'Pipeline was skipped',
+ manual: 'Pipeline needs to be manually started',
+ scheduled: 'Pipeline is scheduled to run'
+ }.freeze
UnknownStatusError = Class.new(StandardError)
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index b468415c4c7..829b2a6ef21 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -33,7 +33,7 @@ module CounterAttribute
extend AfterCommitQueue
include Gitlab::ExclusiveLeaseHelpers
- LUA_STEAL_INCREMENT_SCRIPT = <<~EOS.freeze
+ LUA_STEAL_INCREMENT_SCRIPT = <<~EOS
local increment_key, flushed_key = KEYS[1], KEYS[2]
local increment_value = redis.call("get", increment_key) or 0
local flushed_value = redis.call("incrby", flushed_key, increment_value)
diff --git a/app/models/concerns/deprecated_assignee.rb b/app/models/concerns/deprecated_assignee.rb
index 7f12ce39c96..3f557ee9b48 100644
--- a/app/models/concerns/deprecated_assignee.rb
+++ b/app/models/concerns/deprecated_assignee.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# This module handles backward compatibility for import/export of Merge Requests after
+# This module handles backward compatibility for import/export of merge requests after
# multiple assignees feature was introduced. Also, it handles the scenarios where
# the #26496 background migration hasn't finished yet.
# Ideally, most of this code should be removed at #59457.
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 48b4a402974..de17f50cd29 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -20,6 +20,8 @@ module Enums
scheduler_failure: 11,
data_integrity_failure: 12,
forward_deployment_failure: 13,
+ user_blocked: 14,
+ project_deleted: 15,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index f8314d8b429..fdc48d09db2 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -13,7 +13,9 @@ module Enums
activity_limit_exceeded: 20,
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22,
- deployments_limit_exceeded: 23
+ deployments_limit_exceeded: 23,
+ user_blocked: 24,
+ project_deleted: 25
}
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index b9ad78c14fd..774cda2c3e8 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -77,9 +77,14 @@ module HasRepository
def default_branch_from_preferences
return unless empty_repo?
- group_branch_default_name = group&.default_branch_name if respond_to?(:group)
+ (default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence
+ end
+
+ def default_branch_from_group_preferences
+ return unless respond_to?(:group)
+ return unless group
- (group_branch_default_name || Gitlab::CurrentSettings.default_branch_name).presence
+ group.default_branch_name || group.root_ancestor.default_branch_name
end
def reload_default_branch
diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb
new file mode 100644
index 00000000000..90f9876de95
--- /dev/null
+++ b/app/models/concerns/has_timelogs_report.rb
@@ -0,0 +1,20 @@
+# 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).for_issues_in_group(self)
+ end
+end
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb
index 9d446841a9f..5e53f13be95 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/integration.rb
@@ -6,12 +6,12 @@ module Integration
class_methods do
def with_custom_integration_for(integration, page = nil, per = nil)
custom_integration_project_ids = Service
+ .select(:project_id)
.where(type: integration.type)
.where(inherit_from_id: nil)
- .distinct # Required until https://gitlab.com/gitlab-org/gitlab/-/issues/207385
+ .where.not(project_id: nil)
.page(page)
.per(per)
- .pluck(:project_id)
Project.where(id: custom_integration_project_ids)
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index e1be0665452..1e44321e148 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -65,7 +65,7 @@ module Issuable
has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, through: :label_links
- has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :todos, as: :target
has_one :metrics, inverse_of: model_name.singular.to_sym, autosave: true
@@ -137,6 +137,14 @@ module Issuable
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
+ scope :includes_for_bulk_update, -> do
+ associations = %i[author assignees epic group labels metrics project source_project target_project].select do |association|
+ reflect_on_association(association)
+ end
+
+ includes(*associations)
+ end
+
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
@@ -324,7 +332,7 @@ module Issuable
# This prevents errors when ignored columns are present in the database.
issuable_columns = with_cte ? issue_grouping_columns(use_cte: with_cte) : "#{table_name}.*"
- extra_select_columns = extra_select_columns.unshift("(#{highest_priority}) AS highest_priority")
+ extra_select_columns.unshift("(#{highest_priority}) AS highest_priority")
select(issuable_columns)
.select(extra_select_columns)
@@ -437,7 +445,7 @@ module Issuable
end
def subscribed_without_subscriptions?(user, project)
- participants(user).include?(user)
+ participant?(user)
end
def can_assign_epic?(user)
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index e624b9aa356..59e0ed75d2d 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -73,6 +73,10 @@ module LoadedInGroupList
def member_count
@member_count ||= try(:preloaded_member_count) || members.count
end
+
+ def guest_count
+ @guest_count ||= members.guests.count
+ end
end
LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods')
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index ccb334343ff..d42417bb6c1 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -39,11 +39,13 @@ module Milestoneable
private
def milestone_is_valid
- errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
+ errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && !milestone_available?
end
end
def milestone_available?
+ return true if milestone_id.blank?
+
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 5f24564dc56..eaf64f2541d 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Milestoneish
- DISPLAY_ISSUES_LIMIT = 3000
+ DISPLAY_ISSUES_LIMIT = 500
def total_issues_count
@total_issues_count ||= Milestones::IssuesCountService.new(self).count
@@ -15,6 +15,10 @@ module Milestoneish
total_issues_count - closed_issues_count
end
+ def total_merge_requests_count
+ @total_merge_request_count ||= Milestones::MergeRequestsCountService.new(self).count
+ end
+
def complete?
total_issues_count > 0 && total_issues_count == closed_issues_count
end
diff --git a/app/models/concerns/object_storable.rb b/app/models/concerns/object_storable.rb
new file mode 100644
index 00000000000..c13dddc0b88
--- /dev/null
+++ b/app/models/concerns/object_storable.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ObjectStorable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :with_files_stored_locally, -> { where(klass::STORE_COLUMN => ObjectStorage::Store::LOCAL) }
+ scope :with_files_stored_remotely, -> { where(klass::STORE_COLUMN => ObjectStorage::Store::REMOTE) }
+ end
+end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index af105629398..acd654bd229 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -56,18 +56,34 @@ module Participable
# This method processes attributes of objects in breadth-first order.
#
# Returns an Array of User instances.
- def participants(current_user = nil)
- all_participants[current_user]
+ def participants(user = nil)
+ filtered_participants_hash[user]
+ end
+
+ # Checks if the user is a participant in a discussion.
+ #
+ # This method processes attributes of objects in breadth-first order.
+ #
+ # Returns a Boolean.
+ def participant?(user)
+ can_read_participable?(user) &&
+ all_participants_hash[user].include?(user)
end
private
- def all_participants
- @all_participants ||= Hash.new do |hash, user|
+ def all_participants_hash
+ @all_participants_hash ||= Hash.new do |hash, user|
hash[user] = raw_participants(user)
end
end
+ def filtered_participants_hash
+ @filtered_participants_hash ||= Hash.new do |hash, user|
+ hash[user] = filter_by_ability(all_participants_hash[user])
+ end
+ end
+
def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
@@ -98,8 +114,6 @@ module Participable
end
participants.merge(ext.users)
-
- filter_by_ability(participants)
end
def filter_by_ability(participants)
@@ -110,6 +124,15 @@ module Participable
Ability.users_that_can_read_project(participants.to_a, project)
end
end
+
+ def can_read_participable?(participant)
+ case self
+ when PersonalSnippet
+ participant.can?(:read_snippet, self)
+ else
+ participant.can?(:read_project, project)
+ end
+ end
end
Participable.prepend_if_ee('EE::Participable')
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 65195a8d5aa..2828ae4a3a9 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -4,7 +4,7 @@ module ProtectedRef
extend ActiveSupport::Concern
included do
- belongs_to :project
+ belongs_to :project, touch: true
validates :name, presence: true
validates :project, presence: true
diff --git a/app/models/concerns/safe_url.rb b/app/models/concerns/safe_url.rb
index febca7d241f..7dce05bddba 100644
--- a/app/models/concerns/safe_url.rb
+++ b/app/models/concerns/safe_url.rb
@@ -3,12 +3,12 @@
module SafeUrl
extend ActiveSupport::Concern
- def safe_url(usernames_whitelist: [])
+ def safe_url(allowed_usernames: [])
return if url.nil?
uri = URI.parse(url)
uri.password = '*****' if uri.password
- uri.user = '*****' if uri.user && !usernames_whitelist.include?(uri.user)
+ uri.user = '*****' if uri.user && allowed_usernames.exclude?(uri.user)
uri.to_s
rescue URI::Error
end
diff --git a/app/models/concerns/sidebars/container_with_html_options.rb b/app/models/concerns/sidebars/container_with_html_options.rb
new file mode 100644
index 00000000000..12ea366c66a
--- /dev/null
+++ b/app/models/concerns/sidebars/container_with_html_options.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module ContainerWithHtmlOptions
+ # The attributes returned from this method
+ # will be applied to helper methods like
+ # `link_to` or the div containing the container.
+ def container_html_options
+ {
+ aria: { label: title }
+ }.merge(extra_container_html_options)
+ end
+
+ # Classes will override mostly this method
+ # and not `container_html_options`.
+ def extra_container_html_options
+ {}
+ end
+
+ # Attributes to pass to the html_options attribute
+ # in the helper method that sets the active class
+ # on each element.
+ def nav_link_html_options
+ {}
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ # The attributes returned from this method
+ # will be applied right next to the title,
+ # for example in the span that renders the title.
+ def title_html_options
+ {}
+ end
+
+ def link
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_active_routes.rb b/app/models/concerns/sidebars/has_active_routes.rb
new file mode 100644
index 00000000000..e7a153f067a
--- /dev/null
+++ b/app/models/concerns/sidebars/has_active_routes.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module HasActiveRoutes
+ # This method will indicate for which paths or
+ # controllers, the menu or menu item should
+ # be set as active.
+ #
+ # The returned values are passed to the `nav_link` helper method,
+ # so the params can be either `path`, `page`, `controller`.
+ # Param 'action' is not supported.
+ def active_routes
+ {}
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_hint.rb b/app/models/concerns/sidebars/has_hint.rb
new file mode 100644
index 00000000000..21dca39dca0
--- /dev/null
+++ b/app/models/concerns/sidebars/has_hint.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This module has the necessary methods to store
+# hints for menus. Hints are elements displayed
+# when the user hover the menu item.
+module Sidebars
+ module HasHint
+ def show_hint?
+ false
+ end
+
+ def hint_html_options
+ {}
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_icon.rb b/app/models/concerns/sidebars/has_icon.rb
new file mode 100644
index 00000000000..d1a87918285
--- /dev/null
+++ b/app/models/concerns/sidebars/has_icon.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# This module has the necessary methods to show
+# sprites or images next to the menu item.
+module Sidebars
+ module HasIcon
+ def sprite_icon
+ nil
+ end
+
+ def sprite_icon_html_options
+ {}
+ end
+
+ def image_path
+ nil
+ end
+
+ def image_html_options
+ {}
+ end
+
+ def icon_or_image?
+ sprite_icon || image_path
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/has_pill.rb b/app/models/concerns/sidebars/has_pill.rb
new file mode 100644
index 00000000000..ad7064fe63d
--- /dev/null
+++ b/app/models/concerns/sidebars/has_pill.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# This module introduces the logic to show the "pill" element
+# next to the menu item, indicating the a count.
+module Sidebars
+ module HasPill
+ def has_pill?
+ false
+ end
+
+ # In this method we will need to provide the query
+ # to retrieve the elements count
+ def pill_count
+ raise NotImplementedError
+ end
+
+ def pill_html_options
+ {}
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/positionable_list.rb b/app/models/concerns/sidebars/positionable_list.rb
new file mode 100644
index 00000000000..30830d547f3
--- /dev/null
+++ b/app/models/concerns/sidebars/positionable_list.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# This module handles elements in a list. All elements
+# must have a different class
+module Sidebars
+ module PositionableList
+ def add_element(list, element)
+ list << element
+ end
+
+ def insert_element_before(list, before_element, new_element)
+ index = index_of(list, before_element)
+
+ if index
+ list.insert(index, new_element)
+ else
+ list.unshift(new_element)
+ end
+ end
+
+ def insert_element_after(list, after_element, new_element)
+ index = index_of(list, after_element)
+
+ if index
+ list.insert(index + 1, new_element)
+ else
+ add_element(list, new_element)
+ end
+ end
+
+ private
+
+ def index_of(list, element)
+ list.index { |e| e.is_a?(element) }
+ end
+ end
+end
diff --git a/app/models/concerns/sidebars/renderable.rb b/app/models/concerns/sidebars/renderable.rb
new file mode 100644
index 00000000000..a3976af8515
--- /dev/null
+++ b/app/models/concerns/sidebars/renderable.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Renderable
+ # This method will control whether the menu or menu_item
+ # should be rendered. It will be overriden by specific
+ # classes.
+ def render?
+ true
+ end
+ end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 4fe2a0e1827..9f5e9b2bb57 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -9,6 +9,7 @@ module Sortable
included do
scope :with_order_id_desc, -> { order(self.arel_table['id'].desc) }
+ scope :with_order_id_asc, -> { order(self.arel_table['id'].asc) }
scope :order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :order_id_asc, -> { reorder(self.arel_table['id'].asc) }
scope :order_created_desc, -> { reorder(self.arel_table['created_at'].desc) }
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 33e9e0e38fb..5a10ea7a248 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -17,13 +17,37 @@ module Subscribable
def subscribed?(user, project = nil)
return false unless user
- if subscription = subscriptions.find_by(user: user, project: project)
+ if (subscription = lazy_subscription(user, project)&.itself)
subscription.subscribed
else
subscribed_without_subscriptions?(user, project)
end
end
+ def lazy_subscription(user, project = nil)
+ return unless user
+
+ # handle project and group labels as well as issuable subscriptions
+ subscribable_type = self.class.ancestors.include?(Label) ? 'Label' : self.class.name
+ BatchLoader.for(id: id, subscribable_type: subscribable_type, project_id: project&.id).batch do |items, loader|
+ values = items.each_with_object({ ids: Set.new, subscribable_types: Set.new, project_ids: Set.new }) do |item, result|
+ result[:ids] << item[:id]
+ result[:subscribable_types] << item[:subscribable_type]
+ result[:project_ids] << item[:project_id]
+ end
+
+ subscriptions = Subscription.where(subscribable_id: values[:ids], subscribable_type: values[:subscribable_types], project_id: values[:project_ids], user: user)
+
+ subscriptions.each do |subscription|
+ loader.call({
+ id: subscription.subscribable_id,
+ subscribable_type: subscription.subscribable_type,
+ project_id: subscription.project_id
+ }, subscription)
+ end
+ end
+ end
+
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
def subscribed_without_subscriptions?(user, project)
@@ -41,8 +65,10 @@ module Subscribable
def toggle_subscription(user, project = nil)
unsubscribe_from_other_levels(user, project)
+ new_value = !subscribed?(user, project)
+
find_or_initialize_subscription(user, project)
- .update(subscribed: !subscribed?(user, project))
+ .update(subscribed: new_value)
end
def subscribe(user, project = nil)
@@ -83,6 +109,8 @@ module Subscribable
end
def find_or_initialize_subscription(user, project)
+ BatchLoader::Executor.clear_current
+
subscriptions
.find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 5debfa6f834..d8867177059 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -30,7 +30,8 @@ module Taskable
end
def self.get_updated_tasks(old_content:, new_content:)
- old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content)
+ old_tasks = get_tasks(old_content)
+ new_tasks = get_tasks(new_content)
new_tasks.select.with_index do |new_task, i|
old_task = old_tasks[i]
diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
index 672402ee4d6..50a2613bb10 100644
--- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb
+++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
@@ -42,14 +42,14 @@ module TokenAuthenticatableStrategies
return insecure_strategy.get_token(instance) if migrating?
encrypted_token = instance.read_attribute(encrypted_field)
- token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+ token = EncryptionHelper.decrypt_token(encrypted_token)
token || (insecure_strategy.get_token(instance) if optional?)
end
def set_token(instance, token)
raise ArgumentError unless token.present?
- instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ instance[encrypted_field] = EncryptionHelper.encrypt_token(token)
instance[token_field] = token if migrating?
instance[token_field] = nil if optional?
token
@@ -85,16 +85,9 @@ module TokenAuthenticatableStrategies
end
def find_by_encrypted_token(token, unscoped)
- nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
- encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce)
-
- relation(unscoped).find_by(encrypted_field => encrypted_value)
- end
-
- def find_hashed_iv(token)
- token_record = TokenWithIv.find_by_plaintext_token(token)
-
- token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC
+ encrypted_value = EncryptionHelper.encrypt_token(token)
+ token_encrypted_with_static_iv = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ relation(unscoped).find_by(encrypted_field => [encrypted_value, token_encrypted_with_static_iv])
end
def insecure_strategy
diff --git a/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb b/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
new file mode 100644
index 00000000000..25c050820d6
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/encryption_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class EncryptionHelper
+ 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
+
+ # The pattern of the token is "#{DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv_of_12_characters}"
+ if token.start_with?(DYNAMIC_NONCE_IDENTIFIER) && token.size > NONCE_SIZE + DYNAMIC_NONCE_IDENTIFIER.size
+ token_to_decrypt = token[1...-NONCE_SIZE]
+ iv = token[-NONCE_SIZE..-1]
+
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(token_to_decrypt, nonce: iv)
+ else
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(token)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
new file mode 100644
index 00000000000..cf50305faab
--- /dev/null
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module VulnerabilityFindingHelpers
+ extend ActiveSupport::Concern
+end
+
+VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
new file mode 100644
index 00000000000..f57e3cb0bfb
--- /dev/null
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module VulnerabilityFindingSignatureHelpers
+ extend ActiveSupport::Concern
+end
+
+VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index db5fd167781..25e3b9fe4f0 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -13,8 +13,6 @@ class DeployKey < Key
scope :are_public, -> { where(public: true) }
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) }
- ignore_column :can_push, remove_after: '2019-12-15', remove_with: '12.6'
-
accepts_nested_attributes_for :deploy_keys_projects
def private?
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index f000e474605..d3280403bfd 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -45,6 +45,7 @@ class Deployment < ApplicationRecord
scope :active, -> { where(status: %i[created running]) }
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) }
+ scope :with_api_entity_associations, -> { preload({ deployable: { runner: [], tags: [], user: [], job_artifacts_archive: [] } }) }
scope :finished_after, ->(date) { where('finished_at >= ?', date) }
scope :finished_before, ->(date) { where('finished_at < ?', date) }
@@ -93,11 +94,6 @@ class Deployment < ApplicationRecord
after_transition any => :success do |deployment|
deployment.run_after_commit do
Deployments::UpdateEnvironmentWorker.perform_async(id)
- end
- end
-
- after_transition any => FINISHED_STATUSES do |deployment|
- deployment.run_after_commit do
Deployments::LinkMergeRequestWorker.perform_async(id)
end
end
@@ -175,7 +171,7 @@ class Deployment < ApplicationRecord
end
def commit
- project.commit(sha)
+ @commit ||= project.commit(sha)
end
def commit_title
@@ -225,7 +221,7 @@ class Deployment < ApplicationRecord
end
def update_merge_request_metrics!
- return unless environment.update_merge_request_metrics? && success?
+ return unless environment.production? && success?
merge_requests = project.merge_requests
.joins(:metrics)
@@ -243,29 +239,18 @@ class Deployment < ApplicationRecord
def previous_deployment
@previous_deployment ||=
- project.deployments.joins(:environment)
- .where(environments: { name: self.environment.name }, ref: self.ref)
- .where.not(id: self.id)
- .order(id: :desc)
- .take
- end
-
- def previous_environment_deployment
- project
- .deployments
- .success
- .joins(:environment)
- .where(environments: { name: environment.name })
- .where.not(id: self.id)
- .order(id: :desc)
- .take
+ self.class.for_environment(environment_id)
+ .success
+ .where('id < ?', id)
+ .order(id: :desc)
+ .take
end
def stop_action
return unless on_stop.present?
return unless manual_actions
- @stop_action ||= manual_actions.find_by(name: on_stop)
+ @stop_action ||= manual_actions.find { |action| action.name == self.on_stop }
end
def finished_at
diff --git a/app/models/design_management/design_action.rb b/app/models/design_management/design_action.rb
index 22baa916296..43dcce545d2 100644
--- a/app/models/design_management/design_action.rb
+++ b/app/models/design_management/design_action.rb
@@ -29,7 +29,9 @@ module DesignManagement
# - design [DesignManagement::Design]: the design that was changed
# - action [Symbol]: the action that gitaly performed
def initialize(design, action, content = nil)
- @design, @action, @content = design, action, content
+ @design = design
+ @action = action
+ @content = content
validate!
end
diff --git a/app/models/design_management/design_at_version.rb b/app/models/design_management/design_at_version.rb
index 211211144f4..2f045358914 100644
--- a/app/models/design_management/design_at_version.rb
+++ b/app/models/design_management/design_at_version.rb
@@ -18,7 +18,8 @@ module DesignManagement
validate :design_and_version_have_issue_id
def initialize(design: nil, version: nil)
- @design, @version = design, version
+ @design = design
+ @version = version
end
# The ID, needed by GraphQL types and as part of the Lazy-fetch
diff --git a/app/models/design_management/repository.rb b/app/models/design_management/repository.rb
index 985d6317d5d..2b1e6070e6b 100644
--- a/app/models/design_management/repository.rb
+++ b/app/models/design_management/repository.rb
@@ -8,7 +8,7 @@ module DesignManagement
# repository is entirely GitLab-managed rather than user-facing.
#
# Enable all uploaded files to be stored in LFS.
- MANAGED_GIT_ATTRIBUTES = <<~GA.freeze
+ MANAGED_GIT_ATTRIBUTES = <<~GA
/#{DesignManagement.designs_directory}/* filter=lfs diff=lfs merge=lfs -text
GA
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 49aec8b9720..5cfd8f3ec8e 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -14,7 +14,9 @@ module DesignManagement
attr_reader :sha, :issue_id, :actions
def initialize(sha, issue_id, actions)
- @sha, @issue_id, @actions = sha, issue_id, actions
+ @sha = sha
+ @issue_id = issue_id
+ @actions = actions
end
def message
diff --git a/app/models/environment.rb b/app/models/environment.rb
index d89909a71a2..4ee93b0ba4a 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -11,8 +11,6 @@ class Environment < ApplicationRecord
self.reactive_cache_hard_limit = 10.megabytes
self.reactive_cache_work_type = :external_dependency
- PRODUCTION_ENVIRONMENT_IDENTIFIERS = %w[prod production].freeze
-
belongs_to :project, required: true
use_fast_destroy :all_deployments
@@ -26,13 +24,13 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
- has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
- has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
@@ -88,7 +86,7 @@ class Environment < ApplicationRecord
end
scope :for_project, -> (project) { where(project_id: project) }
- scope :for_tier, -> (tier) { where(tier: tier).where('tier IS NOT NULL') }
+ scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) }
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
scope :unfoldered, -> { where(environment_type: nil) }
scope :with_rank, -> do
@@ -251,10 +249,6 @@ class Environment < ApplicationRecord
last_deployment.try(:created_at)
end
- def update_merge_request_metrics?
- PRODUCTION_ENVIRONMENT_IDENTIFIERS.include?(folder_name.downcase)
- end
-
def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
index ac8b6516d02..7ffb321f2b7 100644
--- a/app/models/experiment.rb
+++ b/app/models/experiment.rb
@@ -21,7 +21,13 @@ class Experiment < ApplicationRecord
# Create or update the recorded experiment_user row for the user in this experiment.
def record_user_and_group(user, group_type, context = {})
experiment_user = experiment_users.find_or_initialize_by(user: user)
- experiment_user.update!(group_type: group_type, context: merged_context(experiment_user, context))
+ experiment_user.assign_attributes(group_type: group_type, context: merged_context(experiment_user, context))
+ # 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
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/324649
+ experiment_user.save! if experiment_user.changed?
+
+ experiment_user
end
def record_conversion_event_for_user(user, context = {})
@@ -32,7 +38,14 @@ class Experiment < ApplicationRecord
end
def record_group_and_variant!(group, variant)
- experiment_subjects.find_or_initialize_by(group: group).update!(variant: variant)
+ experiment_subject = experiment_subjects.find_or_initialize_by(group: group)
+ 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
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/324649
+ experiment_subject.save! if experiment_subject.changed?
+
+ experiment_subject
end
private
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 68b2353556e..36030b80370 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -6,7 +6,8 @@ class ExternalIssue
attr_reader :project
def initialize(issue_identifier, project)
- @issue_identifier, @project = issue_identifier, project
+ @issue_identifier = issue_identifier
+ @project = project
end
def to_s
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index ca6857a14b6..330815ab8c1 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -71,12 +71,12 @@ class GpgKey < ApplicationRecord
end
def emails_with_verified_status
- user_infos.map do |user_info|
+ user_infos.to_h do |user_info|
[
user_info[:email],
user.verified_email?(user_info[:email])
]
- end.to_h
+ end
end
def verified?
diff --git a/app/models/group.rb b/app/models/group.rb
index 9f8a9996f31..2967c1ffc1d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -16,6 +16,7 @@ class Group < Namespace
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
include EachBatch
+ include HasTimelogsReport
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@@ -70,6 +71,7 @@ class Group < Namespace
has_many :group_deploy_keys, through: :group_deploy_keys_groups
has_many :group_deploy_tokens
has_many :deploy_tokens, through: :group_deploy_tokens
+ has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :dependency_proxy_setting, class_name: 'DependencyProxy::GroupSetting'
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
@@ -84,7 +86,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed
- validates :variables, nested_attributes_duplicates: true
+ validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
@@ -178,6 +180,25 @@ class Group < Namespace
groups.drop(1).each { |group| group.root_ancestor = root }
end
+ # Returns the ids of the passed group models where the `emails_disabled`
+ # column is set to true anywhere in the ancestor hierarchy.
+ def ids_with_disabled_email(groups)
+ innner_query = Gitlab::ObjectHierarchy
+ .new(Group.where('id = namespaces_with_emails_disabled.id'))
+ .base_and_ancestors
+ .where(emails_disabled: true)
+ .select('1')
+ .limit(1)
+
+ group_ids = Namespace
+ .from('(SELECT * FROM namespaces) as namespaces_with_emails_disabled')
+ .where(namespaces_with_emails_disabled: { id: groups })
+ .where('EXISTS (?)', innner_query)
+ .pluck(:id)
+
+ Set.new(group_ids)
+ end
+
private
def public_to_user_arel(user)
@@ -325,6 +346,10 @@ class Group < Namespace
members_with_parents.owners.exists?(user_id: user)
end
+ def blocked_owners
+ members.blocked.where(access_level: Gitlab::Access::OWNER)
+ end
+
def has_maintainer?(user)
return false unless user
@@ -337,14 +362,29 @@ class Group < Namespace
# Check if user is a last owner of the group.
def last_owner?(user)
- has_owner?(user) && members_with_parents.owners.size == 1
+ has_owner?(user) && single_owner?
+ end
+
+ def member_last_owner?(member)
+ return member.last_owner unless member.last_owner.nil?
+
+ last_owner?(member.user)
+ end
+
+ def single_owner?
+ members_with_parents.owners.size == 1
end
- def last_blocked_owner?(user)
+ def single_blocked_owner?
+ blocked_owners.size == 1
+ end
+
+ def member_last_blocked_owner?(member)
+ return member.last_blocked_owner unless member.last_blocked_owner.nil?
+
return false if members_with_parents.owners.any?
- blocked_owners = members.blocked.where(access_level: Gitlab::Access::OWNER)
- blocked_owners.size == 1 && blocked_owners.exists?(user_id: user)
+ single_blocked_owner? && blocked_owners.exists?(user_id: member.user)
end
def ldap_synced?
@@ -784,13 +824,11 @@ class Group < Namespace
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)
- if Feature.enabled?(:scoped_group_variables, self, default_enabled: :yaml)
- variables = if environment
- variables.on_environment(environment)
- else
- variables.where(environment_scope: '*')
- end
- end
+ variables = if environment
+ variables.on_environment(environment)
+ else
+ variables.where(environment_scope: '*')
+ end
variables = variables.group_by(&:group_id)
list_of_ids.reverse.flat_map { |group| variables[group.id] }.compact
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index c735e593da7..b56bac58705 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -47,18 +47,10 @@ class InternalId < ApplicationRecord
def update_and_save(&block)
lock!
yield
- update_and_save_counter.increment(usage: usage, changed: last_value_changed?)
save!
last_value
end
- # Instrumentation to track for-update locks
- def update_and_save_counter
- strong_memoize(:update_and_save_counter) do
- Gitlab::Metrics.counter(:gitlab_internal_id_for_update_lock, 'Number of ROW SHARE (FOR UPDATE) locks on individual records from internal_ids')
- end
- end
-
class << self
def track_greatest(subject, scope, usage, new_value, init)
InternalIdGenerator.new(subject, scope, usage, init)
@@ -88,6 +80,8 @@ class InternalId < ApplicationRecord
end
class InternalIdGenerator
+ extend Gitlab::Utils::StrongMemoize
+
# Generate next internal id for a given scope and usage.
#
# For currently supported usages, see #usage enum.
@@ -123,6 +117,8 @@ class InternalId < ApplicationRecord
# init: Block that gets called to initialize InternalId record if not present
# Make sure to not throw exceptions in the absence of records (if this is expected).
def generate
+ self.class.internal_id_transactions_increment(operation: :generate, usage: usage)
+
subject.transaction do
# Create a record in internal_ids if one does not yet exist
# and increment its last value
@@ -138,6 +134,8 @@ class InternalId < ApplicationRecord
def reset(value)
return false unless value
+ self.class.internal_id_transactions_increment(operation: :reset, usage: usage)
+
updated =
InternalId
.where(**scope, usage: usage_value)
@@ -152,6 +150,8 @@ class InternalId < ApplicationRecord
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
+ self.class.internal_id_transactions_increment(operation: :track_greatest, usage: usage)
+
subject.transaction do
record.track_greatest_and_save!(new_value)
end
@@ -162,6 +162,8 @@ class InternalId < ApplicationRecord
end
def with_lock(&block)
+ self.class.internal_id_transactions_increment(operation: :with_lock, usage: usage)
+
record.with_lock(&block)
end
@@ -197,5 +199,22 @@ class InternalId < ApplicationRecord
rescue ActiveRecord::RecordNotUnique
lookup
end
+
+ def self.internal_id_transactions_increment(operation:, usage:)
+ self.internal_id_transactions_total.increment(
+ operation: operation,
+ usage: usage.to_s,
+ in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s
+ )
+ end
+
+ def self.internal_id_transactions_total
+ strong_memoize(:internal_id_transactions_total) do
+ name = :gitlab_internal_id_transactions_total
+ comment = 'Counts all the internal ids happening within transaction'
+
+ Gitlab::Metrics.counter(name, comment)
+ end
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2f2d24cbe93..af78466e6a9 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -24,6 +24,8 @@ class Issue < ApplicationRecord
include Todoable
include FromUnion
+ extend ::Gitlab::Utils::Override
+
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
@@ -88,7 +90,6 @@ class Issue < ApplicationRecord
test_case: 2 ## EE-only
}
- alias_attribute :parent_ids, :project_id
alias_method :issuing_parent, :project
alias_attribute :external_author, :service_desk_reply_to
@@ -113,8 +114,8 @@ class Issue < ApplicationRecord
scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
- scope :with_web_entity_associations, -> { preload(:author, :project) }
- scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
+ scope :with_web_entity_associations, -> { preload(:author, project: [:project_feature, :route, namespace: :route]) }
+ scope :preload_awardable, -> { preload(:award_emoji) }
scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
@@ -191,7 +192,8 @@ class Issue < ApplicationRecord
end
def self.relative_positioning_query_base(issue)
- in_projects(issue.parent_ids)
+ projects = issue.project.group&.root_ancestor&.all_projects || issue.project
+ in_projects(projects)
end
def self.relative_positioning_parent_column
@@ -342,6 +344,8 @@ class Issue < ApplicationRecord
.preload(preload)
.reorder('issue_link_id')
+ related_issues = yield related_issues if block_given?
+
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
current_user,
@@ -446,10 +450,20 @@ class Issue < ApplicationRecord
issue_email_participants.pluck(IssueEmailParticipant.arel_table[:email].lower)
end
+ def issue_assignee_user_ids
+ issue_assignees.pluck(:user_id)
+ end
+
private
+ # Ensure that the metrics association is safely created and respecting the unique constraint on issue_id
+ override :ensure_metrics
def ensure_metrics
- super
+ if !association(:metrics).loaded? || metrics.blank?
+ metrics_record = Issue::Metrics.safe_find_or_create_by(issue: self)
+ self.metrics = metrics_record
+ end
+
metrics.record!
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 18fa8aaaa16..131416d1bee 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -43,6 +43,8 @@ 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"]) }
+ 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
where(type: ['Key', nil])
diff --git a/app/models/list.rb b/app/models/list.rb
index e1954ed72c4..d72afbaee69 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -14,7 +14,6 @@ class List < ApplicationRecord
validates :label_id, uniqueness: { scope: :board_id }, if: :label?
scope :preload_associated_models, -> { preload(:board, label: :priorities) }
- scope :without_types, ->(list_types) { where.not(list_type: list_types) }
alias_method :preferences, :list_user_preferences
diff --git a/app/models/member.rb b/app/models/member.rb
index 38574d67cb6..e978552592d 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -137,6 +137,12 @@ class Member < ApplicationRecord
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
scope :including_source, -> { includes(:source) }
+ scope :distinct_on_user_with_max_access_level, -> do
+ distinct_members = select('DISTINCT ON (user_id, invite_email) *')
+ .order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
+ Member.from(distinct_members, :members)
+ end
+
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
@@ -278,10 +284,16 @@ class Member < ApplicationRecord
Gitlab::Access.sym_options
end
+ def valid_email?(email)
+ Devise.email_regexp.match?(email)
+ end
+
private
def parse_users_list(source, list)
- emails, user_ids, users = [], [], []
+ emails = []
+ user_ids = []
+ users = []
existing_members = {}
list.each do |item|
@@ -299,6 +311,7 @@ class Member < ApplicationRecord
if user_ids.present?
users.concat(User.where(id: user_ids))
+ # the below will automatically discard invalid user_ids
existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index c30f6dc81ee..0f9fdd230ff 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -7,7 +7,7 @@ class GroupMember < Member
SOURCE_TYPE = 'Namespace'
belongs_to :group, foreign_key: 'source_id'
-
+ alias_attribute :namespace_id, :source_id
delegate :update_two_factor_requirement, to: :user
# Make sure group member points only to group as it source
@@ -26,6 +26,8 @@ class GroupMember < Member
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
+ attr_accessor :last_owner, :last_blocked_owner
+
def self.access_level_roles
Gitlab::Access.options_with_owner
end
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
new file mode 100644
index 00000000000..64decb1df36
--- /dev/null
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Members
+ 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
+
+ members.each { |member| set_last_owner(member) }
+ end
+
+ private
+
+ attr_reader :group, :members, :last_blocked_owner, :group_single_owner
+
+ 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 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 member_ids
+ @members_ids ||= members.pluck(:id)
+ end
+
+ def owners
+ @owners ||= group.members_with_parents.owners.load
+ end
+ end
+end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 833b27756ab..9a86b3a3fd9 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -5,6 +5,8 @@ class ProjectMember < Member
belongs_to :project, foreign_key: 'source_id'
+ delegate :namespace_id, to: :project
+
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\AProject\z/ }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7efdd79ae1c..e7f3762b9a3 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::CompareCodequalityReportsService' => ->(project) { ::Gitlab::Ci::Features.display_codequality_backend_comparison?(project) }
+ 'Ci::CompareCodequalityReportsService' => ->(project) { true }
}.freeze
belongs_to :target_project, class_name: "Project"
@@ -276,6 +276,9 @@ class MergeRequest < ApplicationRecord
scope :by_squash_commit_sha, -> (sha) do
where(squash_commit_sha: sha)
end
+ scope :by_merge_or_squash_commit_sha, -> (sha) do
+ from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha)])
+ end
scope :by_related_commit_sha, -> (sha) do
from_union(
[
@@ -285,14 +288,20 @@ class MergeRequest < ApplicationRecord
]
)
end
- scope :by_cherry_pick_sha, -> (sha) do
- joins(:notes).where(notes: { commit_id: sha })
- end
scope :join_project, -> { joins(:target_project) }
- scope :join_metrics, -> do
+ scope :join_metrics, -> (target_project_id = nil) do
+ # Do not join the relation twice
+ return self if self.arel.join_sources.any? { |join| join.left.try(:name).eql?(MergeRequest::Metrics.table_name) }
+
query = joins(:metrics)
- query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
- query
+
+ project_condition = if target_project_id
+ MergeRequest::Metrics.arel_table[:target_project_id].eq(target_project_id)
+ else
+ MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id])
+ end
+
+ query.where(project_condition)
end
scope :references_project, -> { references(:target_project) }
scope :with_api_entity_associations, -> {
@@ -304,6 +313,7 @@ class MergeRequest < ApplicationRecord
}
scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
+ scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
@@ -346,7 +356,9 @@ class MergeRequest < ApplicationRecord
scope :preload_metrics, -> (relation) { preload(metrics: relation) }
scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) }
scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: :merge_request_diff_commits) }
- scope :with_web_entity_associations, -> { preload(:author, :target_project) }
+ scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) }
+
+ scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) }
scope :with_auto_merge_enabled, -> do
with_state(:opened).where(auto_merge_enabled: true)
@@ -1302,11 +1314,8 @@ class MergeRequest < ApplicationRecord
message.join("\n\n")
end
- # Returns the oldest multi-line commit message, or the MR title if none found
def default_squash_commit_message
- strong_memoize(:default_squash_commit_message) do
- first_multiline_commit&.safe_message || title
- end
+ title
end
# Returns the oldest multi-line commit
@@ -1358,11 +1367,11 @@ class MergeRequest < ApplicationRecord
def environments_for(current_user, latest: false)
return [] unless diff_head_commit
- envs = EnvironmentsFinder.new(target_project, current_user,
+ envs = EnvironmentsByDeploymentsFinder.new(target_project, current_user,
ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute
if source_project
- envs.concat EnvironmentsFinder.new(source_project, current_user,
+ envs.concat EnvironmentsByDeploymentsFinder.new(source_project, current_user,
ref: source_branch, commit: diff_head_commit, find_latest: latest).execute
end
@@ -1555,8 +1564,6 @@ class MergeRequest < ApplicationRecord
end
def has_codequality_reports?
- return false unless ::Gitlab::Ci::Features.display_codequality_backend_comparison?(project)
-
actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports)
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index aa4ddfede99..4cf0e423a15 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -89,6 +89,10 @@ class Milestone < ApplicationRecord
.order(:project_id, :group_id, :due_date).select('DISTINCT ON (project_id, group_id) id')
end
+ def self.with_web_entity_associations
+ preload(:group, project: [:project_feature, group: [:parent], namespace: :route])
+ end
+
def participants
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 3f7ccdb977e..455429608b4 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -13,6 +13,9 @@ class Namespace < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include IgnorableColumns
include Namespaces::Traversal::Recursive
+ include Namespaces::Traversal::Linear
+
+ ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
@@ -43,6 +46,9 @@ class Namespace < ApplicationRecord
has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting'
+ has_one :admin_note, inverse_of: :namespace
+ accepts_nested_attributes_for :admin_note, update_only: true
+
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
presence: true,
@@ -83,11 +89,11 @@ class Namespace < ApplicationRecord
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
- before_save :ensure_delayed_project_removal_assigned_to_namespace_settings, if: :delayed_project_removal_changed?
-
scope :for_user, -> { where('type IS NULL') }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
+ scope :by_parent, -> (parent) { where(parent_id: parent) }
+ scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
@@ -107,7 +113,7 @@ class Namespace < ApplicationRecord
# Make sure that the name is same as strong_memoize name in root_ancestor
# method
- attr_writer :root_ancestor
+ attr_writer :root_ancestor, :emails_disabled_memoized
class << self
def by_path(path)
@@ -235,7 +241,7 @@ class Namespace < ApplicationRecord
# any ancestor can disable emails for all descendants
def emails_disabled?
- strong_memoize(:emails_disabled) do
+ strong_memoize(:emails_disabled_memoized) do
if parent_id
self_and_ancestors.where(emails_disabled: true).exists?
else
@@ -260,13 +266,8 @@ class Namespace < ApplicationRecord
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
- return Project.where(namespace: self) if user?
-
- if Feature.enabled?(:recursive_namespace_lookup_as_inner_join, self)
- Project.joins("INNER JOIN (#{self_and_descendants.select(:id).to_sql}) namespaces ON namespaces.id=projects.namespace_id")
- else
- Project.where(namespace: self_and_descendants)
- end
+ namespace = user? ? self : self_and_descendants
+ Project.where(namespace: namespace)
end
# Includes pipelines from this namespace and pipelines from all subgroups
@@ -288,8 +289,13 @@ class Namespace < ApplicationRecord
false
end
+ # Deprecated, use #licensed_feature_available? instead. Remove once Namespace#feature_available? isn't used anymore.
+ def feature_available?(feature)
+ licensed_feature_available?(feature)
+ end
+
# Overridden in EE::Namespace
- def feature_available?(_feature)
+ def licensed_feature_available?(_feature)
false
end
@@ -347,6 +353,10 @@ class Namespace < ApplicationRecord
Plan.default
end
+ def paid?
+ root? && actual_plan.paid?
+ end
+
def actual_limits
# We default to PlanLimits.new otherwise a lot of specs would fail
# On production each plan should already have associated limits record
@@ -412,13 +422,6 @@ class Namespace < ApplicationRecord
private
- def ensure_delayed_project_removal_assigned_to_namespace_settings
- return if Feature.disabled?(:migrate_delayed_project_removal, default_enabled: true)
-
- self.namespace_settings || build_namespace_settings
- namespace_settings.delayed_project_removal = delayed_project_removal
- end
-
def all_projects_with_pages
if all_projects.pages_metadata_not_migrated.exists?
Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation(
diff --git a/app/models/namespace/admin_note.rb b/app/models/namespace/admin_note.rb
new file mode 100644
index 00000000000..3de809d60be
--- /dev/null
+++ b/app/models/namespace/admin_note.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Namespace::AdminNote < ApplicationRecord
+ belongs_to :namespace, inverse_of: :admin_note
+ validates :namespace, presence: true
+ validates :note, length: { maximum: 1000 }
+end
diff --git a/app/models/namespace/traversal_hierarchy.rb b/app/models/namespace/traversal_hierarchy.rb
index cfb6cfdde74..28cf55f7486 100644
--- a/app/models/namespace/traversal_hierarchy.rb
+++ b/app/models/namespace/traversal_hierarchy.rb
@@ -34,17 +34,20 @@ class Namespace
sql = """
UPDATE namespaces
SET traversal_ids = cte.traversal_ids
- FROM (#{recursive_traversal_ids}) as cte
+ FROM (#{recursive_traversal_ids(lock: true)}) as cte
WHERE namespaces.id = cte.id
AND namespaces.traversal_ids <> cte.traversal_ids
"""
Namespace.connection.exec_query(sql)
+ rescue ActiveRecord::Deadlocked
+ db_deadlock_counter.increment(source: 'Namespace#sync_traversal_ids!')
+ raise
end
# Identify all incorrect traversal_ids in the current namespace hierarchy.
- def incorrect_traversal_ids
+ def incorrect_traversal_ids(lock: false)
Namespace
- .joins("INNER JOIN (#{recursive_traversal_ids}) as cte ON namespaces.id = cte.id")
+ .joins("INNER JOIN (#{recursive_traversal_ids(lock: lock)}) as cte ON namespaces.id = cte.id")
.where('namespaces.traversal_ids <> cte.traversal_ids')
end
@@ -55,10 +58,13 @@ class Namespace
#
# Note that the traversal_ids represent a calculated traversal path for the
# namespace and not the value stored within the traversal_ids attribute.
- def recursive_traversal_ids
+ #
+ # Optionally locked with FOR UPDATE to ensure isolation between concurrent
+ # updates of the heirarchy.
+ def recursive_traversal_ids(lock: false)
root_id = Integer(@root.id)
- """
+ sql = <<~SQL
WITH RECURSIVE cte(id, traversal_ids, cycle) AS (
VALUES(#{root_id}, ARRAY[#{root_id}], false)
UNION ALL
@@ -67,7 +73,11 @@ class Namespace
WHERE n.parent_id = cte.id AND NOT cycle
)
SELECT id, traversal_ids FROM cte
- """
+ SQL
+
+ sql += ' FOR UPDATE' if lock
+
+ sql
end
# This is essentially Namespace#root_ancestor which will soon be rewritten
@@ -80,5 +90,9 @@ class Namespace
.reorder(nil)
.find_by(parent_id: nil)
end
+
+ def db_deadlock_counter
+ Gitlab::Metrics.counter(:db_deadlock, 'Counts the times we have deadlocked in the database')
+ end
end
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 50844403d7f..d21f9632e18 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -1,14 +1,20 @@
# frozen_string_literal: true
class NamespaceSetting < ApplicationRecord
+ include CascadingNamespaceSettingAttribute
+
+ cascading_attr :delayed_project_removal
+
belongs_to :namespace, inverse_of: :namespace_settings
validate :default_branch_name_content
validate :allow_mfa_for_group
+ validate :allow_resource_access_token_creation_for_group
before_validation :normalize_default_branch_name
- NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
+ NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
+ :lock_delayed_project_removal, :resource_access_token_creation_allowed].freeze
self.primary_key = :namespace_id
@@ -31,6 +37,12 @@ class NamespaceSetting < ApplicationRecord
errors.add(:allow_mfa_for_subgroups, _('is not allowed since the group is not top-level group.'))
end
end
+
+ def allow_resource_access_token_creation_for_group
+ if namespace&.subgroup? && !resource_access_token_creation_allowed
+ errors.add(:resource_access_token_creation_allowed, _('is not allowed since the group is not top-level group.'))
+ end
+ end
end
NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
new file mode 100644
index 00000000000..dd9ca8d9bea
--- /dev/null
+++ b/app/models/namespaces/traversal/linear.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+#
+# Query a recursively defined namespace hierarchy using linear methods through
+# the traversal_ids attribute.
+#
+# Namespace is a nested hierarchy of one parent to many children. A search
+# using only the parent-child relationships is a slow operation. This process
+# was previously optimized using Postgresql recursive common table expressions
+# (CTE) with acceptable performance. However, it lead to slower than possible
+# performance, and resulted in complicated queries that were difficult to make
+# performant.
+#
+# Instead of searching the hierarchy recursively, we store a `traversal_ids`
+# attribute on each node. The `traversal_ids` is an ordered array of Namespace
+# IDs that define the traversal path from the root Namespace to the current
+# Namespace.
+#
+# For example, suppose we have the following Namespaces:
+#
+# GitLab (id: 1) > Engineering (id: 2) > Manage (id: 3) > Access (id: 4)
+#
+# Then `traversal_ids` for group "Access" is [1, 2, 3, 4]
+#
+# And we can match against other Namespace `traversal_ids` such that:
+#
+# - Ancestors are [1], [1, 2], [1, 2, 3]
+# - Descendants are [1, 2, 3, 4, *]
+# - Root is [1]
+# - Hierarchy is [1, *]
+#
+# Note that this search method works so long as the IDs are unique and the
+# traversal path is ordered from root to leaf nodes.
+#
+# We implement this in the database using Postgresql arrays, indexed by a
+# generalized inverted index (gin).
+module Namespaces
+ module Traversal
+ module Linear
+ extend ActiveSupport::Concern
+
+ UnboundedSearch = Class.new(StandardError)
+
+ included do
+ after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
+ after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
+
+ scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
+ end
+
+ def sync_traversal_ids?
+ Feature.enabled?(:sync_traversal_ids, root_ancestor, default_enabled: :yaml)
+ end
+
+ def use_traversal_ids?
+ Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+ end
+
+ def self_and_descendants
+ if use_traversal_ids?
+ lineage(self)
+ else
+ super
+ end
+ end
+
+ private
+
+ # Update the traversal_ids for the full hierarchy.
+ #
+ # NOTE: self.traversal_ids will be stale. Reload for a fresh record.
+ def sync_traversal_ids
+ # Clear any previously memoized root_ancestor as our ancestors have changed.
+ clear_memoization(:root_ancestor)
+
+ Namespace::TraversalHierarchy.for_namespace(root_ancestor).sync_traversal_ids!
+ end
+
+ # Make sure we drop the STI `type = 'Group'` condition for better performance.
+ # Logically equivalent so long as hierarchies remain homogeneous.
+ def without_sti_condition
+ self.class.unscope(where: :type)
+ end
+
+ # Search this namespace's lineage. Bound inclusively by top node.
+ def lineage(top)
+ raise UnboundedSearch.new('Must bound search by a top') unless top
+
+ without_sti_condition
+ .traversal_ids_contains("{#{top.id}}")
+ end
+ end
+ end
+end
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
index d74b7883830..409438f53d2 100644
--- a/app/models/namespaces/traversal/recursive.rb
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -6,10 +6,14 @@ module Namespaces
extend ActiveSupport::Concern
def root_ancestor
- return self if persisted? && parent_id.nil?
+ return self if parent.nil?
- strong_memoize(:root_ancestor) do
- self_and_ancestors.reorder(nil).find_by(parent_id: nil)
+ if persisted?
+ strong_memoize(:root_ancestor) do
+ self_and_ancestors.reorder(nil).find_by(parent_id: nil)
+ end
+ else
+ parent.root_ancestor
end
end
@@ -18,6 +22,7 @@ module Namespaces
object_hierarchy(self.class.where(id: id))
.all_objects
end
+ alias_method :recursive_self_and_hierarchy, :self_and_hierarchy
# Returns all the ancestors of the current namespaces.
def ancestors
@@ -26,6 +31,7 @@ module Namespaces
object_hierarchy(self.class.where(id: parent_id))
.base_and_ancestors
end
+ alias_method :recursive_ancestors, :ancestors
# returns all ancestors upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
@@ -40,17 +46,20 @@ module Namespaces
object_hierarchy(self.class.where(id: id))
.base_and_ancestors(hierarchy_order: hierarchy_order)
end
+ alias_method :recursive_self_and_ancestors, :self_and_ancestors
# Returns all the descendants of the current namespace.
def descendants
object_hierarchy(self.class.where(parent_id: id))
.base_and_descendants
end
+ alias_method :recursive_descendants, :descendants
def self_and_descendants
object_hierarchy(self.class.where(id: id))
.base_and_descendants
end
+ alias_method :recursive_self_and_descendants, :self_and_descendants
def object_hierarchy(ancestors_base)
Gitlab::ObjectHierarchy.new(ancestors_base, options: { use_distinct: Feature.enabled?(:use_distinct_in_object_hierarchy, self) })
diff --git a/app/models/note.rb b/app/models/note.rb
index fb540d692d1..3e560a09fbd 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -19,6 +19,7 @@ class Note < ApplicationRecord
include Gitlab::SQL::Pattern
include ThrottledTouch
include FromUnion
+ include Sortable
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
@@ -103,12 +104,12 @@ class Note < ApplicationRecord
scope :system, -> { where(system: true) }
scope :user, -> { where(system: false) }
scope :common, -> { where(noteable_type: ["", nil]) }
- scope :fresh, -> { order(created_at: :asc, id: :asc) }
+ scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
- scope :by_updated_at, -> { reorder(:updated_at, :id) }
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
+ scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
scope :inc_relations_for_view, -> do
includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji,
{ system_note_metadata: :description_version }, :note_diff_file, :diff_note_positions, :suggestions)
@@ -135,6 +136,7 @@ class Note < ApplicationRecord
project: [:project_members, :namespace, { group: [:group_members] }])
end
scope :with_metadata, -> { includes(:system_note_metadata) }
+ scope :with_web_entity_associations, -> { preload(:project, :author, :noteable) }
scope :for_note_or_capitalized_note, ->(text) { where(note: [text, text.capitalize]) }
scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
@@ -148,6 +150,8 @@ class Note < ApplicationRecord
after_commit :notify_after_destroy, on: :destroy
class << self
+ extend Gitlab::Utils::Override
+
def model_name
ActiveModel::Name.new(self, nil, 'note')
end
@@ -204,6 +208,17 @@ class Note < ApplicationRecord
def search(query)
fuzzy_search(query, [:note])
end
+
+ # Override the `Sortable` module's `.simple_sorts` to remove name sorting,
+ # as a `Note` does not have any property that correlates to a "name".
+ override :simple_sorts
+ def simple_sorts
+ super.except('name_asc', 'name_desc')
+ end
+
+ def cherry_picked_merge_requests(shas)
+ where(noteable_type: 'MergeRequest', commit_id: shas).select(:noteable_id)
+ end
end
# rubocop: disable CodeReuse/ServiceClass
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 72813b17501..3d049336d44 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class NotificationSetting < ApplicationRecord
+ include FromUnion
+
enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
@@ -30,6 +32,8 @@ class NotificationSetting < ApplicationRecord
scope :preload_source_route, -> { preload(source: [:route]) }
+ scope :order_by_id_asc, -> { order(id: :asc) }
+
# NOTE: Applicable unfound_translations.rb also needs to be updated when below events are changed.
EMAIL_EVENTS = [
:new_release,
diff --git a/app/models/packages/debian/file_entry.rb b/app/models/packages/debian/file_entry.rb
new file mode 100644
index 00000000000..eb66f4acfa9
--- /dev/null
+++ b/app/models/packages/debian/file_entry.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class FileEntry
+ include ActiveModel::Model
+
+ DIGESTS = %i[md5 sha1 sha256].freeze
+ FILENAME_REGEX = %r{\A[a-zA-Z0-9][a-zA-Z0-9_.~+-]*\z}.freeze
+
+ attr_accessor :filename,
+ :size,
+ :md5sum,
+ :section,
+ :priority,
+ :sha1sum,
+ :sha256sum,
+ :package_file
+
+ validates :filename, :size, :md5sum, :section, :priority, :sha1sum, :sha256sum, :package_file, presence: true
+ validates :filename, format: { with: FILENAME_REGEX }
+ validate :valid_package_file_digests, if: -> { md5sum.present? && sha1sum.present? && sha256sum.present? && package_file.present? }
+
+ def component
+ return 'main' if section.blank?
+ return 'main' unless section.include?('/')
+
+ section.split('/')[0]
+ end
+
+ private
+
+ def valid_package_file_digests
+ DIGESTS.each do |digest|
+ package_file_digest = package_file["file_#{digest}"]
+ sum = public_send("#{digest}sum") # rubocop:disable GitlabSecurity/PublicSend
+ next if package_file_digest == sum
+
+ errors.add("#{digest}sum".to_sym, "mismatch for #{filename}: #{package_file_digest} != #{sum}")
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/packages/debian/file_metadatum.rb b/app/models/packages/debian/file_metadatum.rb
index 7c9f4f5f3f1..af51f256e18 100644
--- a/app/models/packages/debian/file_metadatum.rb
+++ b/app/models/packages/debian/file_metadatum.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Packages::Debian::FileMetadatum < ApplicationRecord
+ self.primary_key = :package_file_id
+
belongs_to :package_file, inverse_of: :debian_file_metadatum
validates :package_file, presence: true
diff --git a/app/models/packages/dependency.rb b/app/models/packages/dependency.rb
index a32c3c05bb3..ad3944b5f21 100644
--- a/app/models/packages/dependency.rb
+++ b/app/models/packages/dependency.rb
@@ -7,8 +7,8 @@ class Packages::Dependency < ApplicationRecord
validates :name, uniqueness: { scope: :version_pattern }
NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'
- MAX_STRING_LENGTH = 255.freeze
- MAX_CHUNKED_QUERIES_COUNT = 10.freeze
+ MAX_STRING_LENGTH = 255
+ MAX_CHUNKED_QUERIES_COUNT = 10
def self.ids_for_package_names_and_version_patterns(names_and_version_patterns = {}, chunk_size = 50, max_rows_limit = 200)
names_and_version_patterns.reject! { |key, value| key.size > MAX_STRING_LENGTH || value.size > MAX_STRING_LENGTH }
diff --git a/app/models/packages/go/module_version.rb b/app/models/packages/go/module_version.rb
index a50c78f8e69..fd575e6c96c 100644
--- a/app/models/packages/go/module_version.rb
+++ b/app/models/packages/go/module_version.rb
@@ -4,6 +4,7 @@ module Packages
module Go
class ModuleVersion
include Gitlab::Utils::StrongMemoize
+ include Gitlab::Golang
VALID_TYPES = %i[ref commit pseudo].freeze
@@ -81,6 +82,9 @@ module Packages
end
def valid?
+ # assume the module version is valid if a corresponding Package exists
+ return true if ::Packages::Go::PackageFinder.new(mod.project, mod.name, name).exists?
+
@mod.path_valid?(major) && @mod.gomod_valid?(gomod)
end
diff --git a/app/models/packages/maven/metadatum.rb b/app/models/packages/maven/metadatum.rb
index 7aed274216b..471c4b3a392 100644
--- a/app/models/packages/maven/metadatum.rb
+++ b/app/models/packages/maven/metadatum.rb
@@ -19,6 +19,7 @@ class Packages::Maven::Metadatum < ApplicationRecord
validate :maven_package_type
scope :for_package_ids, -> (package_ids) { where(package_id: package_ids) }
+ scope :with_path, ->(path) { where(path: path) }
scope :order_created, -> { reorder('created_at ASC') }
def self.pluck_app_name
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 993d1123c86..e510432be8f 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -5,6 +5,8 @@ class Packages::Package < ApplicationRecord
include UsageStatistics
include Gitlab::Utils::StrongMemoize
+ DISPLAYABLE_STATUSES = [:default, :error].freeze
+
belongs_to :project
belongs_to :creator, class_name: 'User'
@@ -29,6 +31,7 @@ class Packages::Package < ApplicationRecord
delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan
delegate :codename, :suite, to: :debian_distribution, prefix: :debian_distribution
+ delegate :target_sha, to: :composer_metadatum, prefix: :composer
validates :project, presence: true
validates :name, presence: true
@@ -69,7 +72,7 @@ class Packages::Package < ApplicationRecord
composer: 6, generic: 7, golang: 8, debian: 9,
rubygems: 10 }
- enum status: { default: 0, hidden: 1, processing: 2 }
+ enum status: { default: 0, hidden: 1, processing: 2, error: 3 }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
@@ -79,7 +82,7 @@ class Packages::Package < ApplicationRecord
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
scope :with_status, ->(status) { where(status: status) }
- scope :displayable, -> { with_status(:default) }
+ scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
@@ -135,13 +138,26 @@ class Packages::Package < ApplicationRecord
after_commit :update_composer_cache, on: :destroy, if: -> { composer? }
def self.for_projects(projects)
- return none unless projects.any?
+ unless Feature.enabled?(:maven_packages_group_level_improvements, default_enabled: :yaml)
+ return none unless projects.any?
+ end
where(project_id: projects)
end
- def self.only_maven_packages_with_path(path)
- joins(:maven_metadatum).where(packages_maven_metadata: { path: path })
+ def self.only_maven_packages_with_path(path, use_cte: false)
+ if use_cte && Feature.enabled?(:maven_metadata_by_path_with_optimization_fence, default_enabled: :yaml)
+ # This is an optimization fence which assumes that looking up the Metadatum record by path (globally)
+ # 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.
+
+ inner_query = Packages::Maven::Metadatum.where(path: path).select(:id, :package_id)
+ cte = Gitlab::SQL::CTE.new(:maven_metadata_by_path, inner_query)
+ with(cte.to_arel)
+ .joins('INNER JOIN maven_metadata_by_path ON maven_metadata_by_path.package_id=packages_packages.id')
+ else
+ joins(:maven_metadatum).where(packages_maven_metadata: { path: path })
+ end
end
def self.by_name_and_file_name(name, file_name)
diff --git a/app/models/packages/tag.rb b/app/models/packages/tag.rb
index 771d016daed..14a1ae98ed4 100644
--- a/app/models/packages/tag.rb
+++ b/app/models/packages/tag.rb
@@ -4,7 +4,7 @@ class Packages::Tag < ApplicationRecord
validates :package, :name, presence: true
- FOR_PACKAGES_TAGS_LIMIT = 200.freeze
+ FOR_PACKAGES_TAGS_LIMIT = 200
NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags
scope :preload_package, -> { preload(:package) }
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 33771580be2..3285a1f7f4c 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -50,9 +50,7 @@ module Pages
def zip_source
return unless deployment&.file
- return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project, default_enabled: true)
-
- return if deployment.migrated? && !Feature.enabled?(:pages_serve_from_migrated_zip, project, default_enabled: true)
+ return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project, default_enabled: :yaml)
global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
@@ -74,7 +72,7 @@ module Pages
path: File.join(project.full_path, 'public/')
}
rescue LegacyStorageDisabledError => e
- Gitlab::ErrorTracking.track_exception(e)
+ Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
nil
end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index d67a92af6af..294a4e85d1f 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -14,6 +14,8 @@ class PagesDeployment < ApplicationRecord
scope :older_than, -> (id) { where('id < ?', id) }
scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) }
+ scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
+ scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
diff --git a/app/models/preloaders/labels_preloader.rb b/app/models/preloaders/labels_preloader.rb
new file mode 100644
index 00000000000..427f2869aac
--- /dev/null
+++ b/app/models/preloaders/labels_preloader.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Preloaders
+ # This class preloads the `project`, `group`, and subscription associations for the given
+ # labels, user, and project (if provided). A Label can be of type ProjectLabel or GroupLabel
+ # and the preloader supports both.
+ #
+ # Usage:
+ # labels = Label.where(...)
+ # Preloaders::LabelsPreloader.new(labels, current_user, @project).preload_all
+ # labels.first.project # won't fire any query
+ class LabelsPreloader
+ attr_reader :labels, :user, :project
+
+ def initialize(labels, user, project = nil)
+ @labels = labels
+ @user = user
+ @project = project
+ end
+
+ def preload_all
+ preloader = ActiveRecord::Associations::Preloader.new
+
+ preloader.preload(labels.select {|l| l.is_a? ProjectLabel }, { project: [:project_feature, namespace: :route] })
+ preloader.preload(labels.select {|l| l.is_a? GroupLabel }, { group: :route })
+ labels.each do |label|
+ label.lazy_subscription(user)
+ label.lazy_subscription(user, project) if project.present?
+ end
+ end
+ end
+end
+
+Preloaders::LabelsPreloader.prepend_if_ee('EE::Preloaders::LabelsPreloader')
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
new file mode 100644
index 00000000000..671091480ee
--- /dev/null
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Preloaders
+ # This class preloads the max access level for the user within the given projects and
+ # stores the values in requests store via the ProjectTeam class.
+ class UserMaxAccessLevelInProjectsPreloader
+ def initialize(projects, user)
+ @projects = projects
+ @user = user
+ end
+
+ def execute
+ access_levels = @user
+ .project_authorizations
+ .where(project_id: @projects)
+ .group(:project_id)
+ .maximum(:access_level)
+
+ @projects.each do |project|
+ access_level = access_levels[project.id] || Gitlab::Access::NO_ACCESS
+ ProjectTeam.new(project).write_member_access_for_user_id(@user.id, access_level)
+ end
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index c52eb95bde8..f03e5293b58 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -36,6 +36,8 @@ class Project < ApplicationRecord
include Integration
include Repositories::CanHousekeepRepository
include EachBatch
+ include GitlabRoutingHelper
+
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
@@ -219,7 +221,7 @@ class Project < ApplicationRecord
has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
has_one :service_desk_setting, class_name: 'ServiceDeskSetting'
- # Merge Requests for target project should be removed with it
+ # Merge requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
@@ -517,7 +519,7 @@ class Project < ApplicationRecord
scope :with_packages, -> { joins(:packages) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
- scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
+ scope :joined, ->(user) { where.not(namespace_id: user.namespace_id) }
scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
@@ -577,7 +579,7 @@ class Project < ApplicationRecord
with_issues_available_for_user(user).or(with_merge_requests_available_for_user(user))
end
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
- scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
+ scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }) }
scope :with_limit, -> (maximum) { limit(maximum) }
scope :with_group_runners_enabled, -> do
@@ -621,7 +623,7 @@ class Project < ApplicationRecord
end
def self.with_web_entity_associations
- preload(:project_feature, :route, :creator, :group, namespace: [:route, :owner])
+ preload(:project_feature, :route, :creator, group: :parent, namespace: [:route, :owner])
end
def self.eager_load_namespace_and_owner
@@ -1368,15 +1370,15 @@ class Project < ApplicationRecord
end
def disabled_services
- return %w(datadog) unless Feature.enabled?(:datadog_ci_integration, self)
+ return %w[datadog hipchat] unless Feature.enabled?(:datadog_ci_integration, self)
- []
+ %w[hipchat]
end
def find_or_initialize_service(name)
return if disabled_services.include?(name)
- find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
+ find_service(services, name) || build_from_instance_or_template(name) || build_service(name)
end
# rubocop: disable CodeReuse/ServiceClass
@@ -1713,10 +1715,15 @@ class Project < ApplicationRecord
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
+
def valid_runners_token?(token)
self.runners_token && ActiveSupport::SecurityUtils.secure_compare(token, self.runners_token)
end
@@ -1812,7 +1819,7 @@ class Project < ApplicationRecord
# TODO: remove this method https://gitlab.com/gitlab-org/gitlab/-/issues/320775
# rubocop: disable CodeReuse/ServiceClass
def legacy_remove_pages
- return unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
+ return unless ::Settings.pages.local_store.enabled
# Projects with a missing namespace cannot have their pages removed
return unless namespace
@@ -1848,7 +1855,7 @@ class Project < ApplicationRecord
# where().update_all to perform update in the single transaction with check for null
ProjectPagesMetadatum
.where(project_id: id, pages_deployment_id: nil)
- .update_all(pages_deployment_id: deployment.id)
+ .update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
end
def write_repository_config(gl_full_path: full_path)
@@ -2145,8 +2152,8 @@ class Project < ApplicationRecord
data = repository.route_map_for(sha)
Gitlab::RouteMap.new(data) if data
- rescue Gitlab::RouteMap::FormatError
- nil
+ rescue Gitlab::RouteMap::FormatError
+ nil
end
end
@@ -2165,17 +2172,18 @@ class Project < ApplicationRecord
end
def default_merge_request_target
- return self unless forked_from_project
- return self unless forked_from_project.merge_requests_enabled?
-
- # When our current visibility is more restrictive than the source project,
- # (e.g., the fork is `private` but the parent is `public`), target the less
- # permissive project
- if visibility_level_value < forked_from_project.visibility_level_value
- self
- else
- forked_from_project
- end
+ return self if project_setting.mr_default_target_self
+ return self unless mr_can_target_upstream?
+
+ forked_from_project
+ end
+
+ def mr_can_target_upstream?
+ # When our current visibility is more restrictive than the upstream project,
+ # (e.g., the fork is `private` but the parent is `public`), don't allow target upstream
+ forked_from_project &&
+ forked_from_project.merge_requests_enabled? &&
+ forked_from_project.visibility_level_value <= visibility_level_value
end
def multiple_issue_boards_available?
@@ -2322,6 +2330,11 @@ class Project < ApplicationRecord
.external_authorization_service_default_label
end
+ # Overridden in EE::Project
+ def licensed_feature_available?(_feature)
+ false
+ end
+
def licensed_features
[]
end
@@ -2584,6 +2597,10 @@ class Project < ApplicationRecord
return Service.build_from_integration(template, project_id: id) if template
end
+ def build_service(name)
+ "#{name}_service".classify.constantize.new(project_id: id)
+ end
+
def services_templates
@services_templates ||= Service.for_template
end
@@ -2734,9 +2751,11 @@ class Project < ApplicationRecord
end
def active_runners_with_tags
- strong_memoize(:active_runners_with_tags) do
- active_runners.with_tags
- end
+ @active_runners_with_tags ||= active_runners.with_tags
+ end
+
+ def online_runners_with_tags
+ @online_runners_with_tags ||= active_runners_with_tags.online
end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index a598bf3f60c..15f6bedfc2e 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -3,6 +3,7 @@
class ProjectFeature < ApplicationRecord
include Featurable
+ # When updating this array, make sure to update rubocop/cop/gitlab/feature_available_usage.rb as well.
FEATURES = %i[
issues
forking
@@ -19,7 +20,7 @@ class ProjectFeature < ApplicationRecord
container_registry
].freeze
- EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance]).freeze
+ EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze
set_available_features(FEATURES)
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index 4f445758653..02051310af7 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -20,12 +20,29 @@ class ProjectFeatureUsage < ApplicationRecord
end
def log_jira_dvcs_integration_usage(cloud: true)
- transaction(requires_new: true) do
- save unless persisted?
- touch(self.class.jira_dvcs_integration_field(cloud: cloud))
- end
+ 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)
+
+ persist_jira_dvcs_usage(integration_field)
+ end
+
+ private
+
+ def updated_today?(integration_field)
+ self[integration_field].present? && self[integration_field].today?
+ end
+
+ def persist_jira_dvcs_usage(integration_field)
+ assign_attributes(integration_field => Time.current)
+ save
rescue ActiveRecord::RecordNotUnique
reset
retry
end
end
+
+ProjectFeatureUsage.prepend_if_ee('EE::ProjectFeatureUsage')
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index c4fcdcc05c5..f31bf931a41 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -3,6 +3,8 @@
require 'asana'
class AsanaService < Service
+ include ActionView::Helpers::UrlHelper
+
prop_accessor :api_key, :restrict_to_branch
validates :api_key, presence: true, if: :activated?
@@ -11,20 +13,12 @@ class AsanaService < Service
end
def description
- s_('AsanaService|Asana - Teamwork without email')
+ s_('AsanaService|Add commit messages as comments to Asana tasks')
end
def help
- 'This service adds commit messages as comments to Asana tasks.
-Once enabled, commit messages are checked for Asana task URLs
-(for example, `https://app.asana.com/0/123456/987654`) or task IDs
-starting with # (for example, `#987654`). Every task ID found will
-get the commit comment added to it.
-
-You can also close a task with a message containing: `fix #123456`.
-
-You can create a Personal Access Token here:
-https://app.asana.com/0/developer-console'
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -36,13 +30,17 @@ https://app.asana.com/0/developer-console'
{
type: 'text',
name: 'api_key',
- placeholder: s_('AsanaService|User Personal Access Token. User must have access to task, all comments will be attributed to this user.'),
+ title: 'API key',
+ help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
+ # Example Personal Access Token from Asana docs
+ placeholder: '0/68a9e79b868c6789e79a124c30b0',
required: true
},
{
type: 'text',
name: 'restrict_to_branch',
- placeholder: s_('AsanaService|Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.')
+ title: 'Restrict to branch (optional)',
+ help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
}
]
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index 60575e45a90..8845fb99605 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -9,7 +9,7 @@ class AssemblaService < Service
end
def description
- 'Project Management Software (Source Commits Endpoint)'
+ _('Manage projects.')
end
def self.to_param
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 8c1f4fef09b..a892d1a4314 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class BambooService < CiService
+ include ActionView::Helpers::UrlHelper
include ReactiveService
prop_accessor :bamboo_url, :build_key, :username, :password
@@ -31,15 +32,16 @@ class BambooService < CiService
end
def title
- s_('BambooService|Atlassian Bamboo CI')
+ s_('BambooService|Atlassian Bamboo')
end
def description
- s_('BambooService|A continuous integration and build server')
+ s_('BambooService|Use the Atlassian Bamboo CI/CD server with GitLab.')
end
def help
- s_('BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo.')
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
+ s_('BambooService|Use Atlassian Bamboo to run CI/CD pipelines. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -48,13 +50,32 @@ class BambooService < CiService
def fields
[
- { type: 'text', name: 'bamboo_url',
- placeholder: s_('BambooService|Bamboo root URL like https://bamboo.example.com'), required: true },
- { type: 'text', name: 'build_key',
- placeholder: s_('BambooService|Bamboo build plan key like KEY'), required: true },
- { type: 'text', name: 'username',
- placeholder: s_('BambooService|A user with API access, if applicable') },
- { type: 'password', name: 'password' }
+ {
+ type: 'text',
+ name: 'bamboo_url',
+ title: s_('BambooService|Bamboo URL'),
+ placeholder: s_('https://bamboo.example.com'),
+ help: s_('BambooService|Bamboo service root URL.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_key',
+ placeholder: s_('KEY'),
+ help: s_('BambooService|Bamboo build plan key.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ help: s_('BambooService|The user with API access to the Bamboo server.')
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
]
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index b9916a54d75..e45bb9b8ce1 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -28,7 +28,7 @@ module ChatMessage
def activity
{
- title: "Merge Request #{state_or_action_text} by #{user_combined_name}",
+ title: "Merge request #{state_or_action_text} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index cf7cad09676..4a99842b4d5 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -61,11 +61,11 @@ class ChatNotificationService < Service
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true }.freeze,
- { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }.freeze,
- { type: 'checkbox', name: 'notify_only_broken_pipelines' }.freeze,
+ { 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: 'e.g. ~backend', help: 'Only supported for issue, merge request and note events.' }.freeze
+ { type: 'text', name: 'labels_to_be_notified', placeholder: '~backend,~frontend', help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' }.freeze
].freeze
end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 47106d7bdbb..29edb9ec16f 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -2,7 +2,7 @@
# Base class for CI services
# List methods you need to implement to get your CI service
-# working with GitLab Merge Requests
+# working with GitLab merge requests
class CiService < Service
default_value_for :category, 'ci'
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index fc58ba27c3d..aab8661ec55 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -17,9 +17,9 @@ class CustomIssueTrackerService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
]
end
end
diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb
index a48dea71645..9a2d99c46c9 100644
--- a/app/models/project_services/datadog_service.rb
+++ b/app/models/project_services/datadog_service.rb
@@ -78,7 +78,9 @@ class DatadogService < Service
{
type: 'password',
name: 'api_key',
- title: 'API key',
+ title: _('API key'),
+ non_empty_password_title: s_('ProjectService|Enter new API key'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
required: true
},
diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb
index 37bbb9b8752..d7adf63fde4 100644
--- a/app/models/project_services/discord_service.rb
+++ b/app/models/project_services/discord_service.rb
@@ -3,6 +3,8 @@
require "discordrb/webhooks"
class DiscordService < ChatNotificationService
+ include ActionView::Helpers::UrlHelper
+
ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
def title
@@ -10,7 +12,7 @@ class DiscordService < ChatNotificationService
end
def description
- s_("DiscordService|Receive event notifications in Discord")
+ s_("DiscordService|Send notifications about project events to a Discord channel.")
end
def self.to_param
@@ -18,13 +20,8 @@ class DiscordService < ChatNotificationService
end
def help
- "This service sends notifications about project events to Discord channels.<br />
- To set up this service:
- <ol>
- <li><a href='https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks'>Setup a custom Incoming Webhook</a>.</li>
- <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
- <li>Select events below to enable notifications.</li>
- </ol>"
+ docs_link = link_to _('How do I set up 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)
@@ -36,13 +33,12 @@ class DiscordService < ChatNotificationService
end
def self.supported_events
- %w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
+ %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://discordapp.com/api/webhooks/…" },
+ { 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 }
]
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 5a49f780d46..ab1ba768a8f 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -79,21 +79,25 @@ class DroneCiService < CiService
end
def title
- 'Drone CI'
+ 'Drone'
end
def description
- 'Drone is a Continuous Integration platform built on Docker, written in Go'
+ 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', placeholder: 'Drone CI project specific token', required: true },
- { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com', required: true },
+ { 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
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 01d8647d439..cdb69684d16 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -3,10 +3,19 @@
class EmailsOnPushService < Service
include NotificationBranchSelection
+ RECIPIENTS_LIMIT = 750
+
boolean_accessor :send_from_committer_email
boolean_accessor :disable_diffs
prop_accessor :recipients, :branches_to_be_notified
- validates :recipients, presence: true, if: :valid_recipients?
+ validates :recipients, presence: true, if: :validate_recipients?
+ validate :number_of_recipients_within_limit, if: :validate_recipients?
+
+ def self.valid_recipients(recipients)
+ recipients.split.select do |recipient|
+ recipient.include?('@')
+ end.uniq(&:downcase)
+ end
def title
s_('EmailsOnPushService|Emails on push')
@@ -63,11 +72,26 @@ class EmailsOnPushService < Service
domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
[
{ type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"),
- help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. %{domains}).") % { domains: domains } },
+ help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
{ type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
- { type: 'textarea', name: 'recipients', placeholder: s_('EmailsOnPushService|Emails separated by whitespace') }
+ {
+ type: 'textarea',
+ name: 'recipients',
+ placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'),
+ help: s_('EmailsOnPushService|Emails separated by whitespace.')
+ }
]
end
+
+ private
+
+ def number_of_recipients_within_limit
+ return if recipients.blank?
+
+ if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
+ errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ end
+ end
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 0a09000fff4..c41783d1af4 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
class ExternalWikiService < Service
+ 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')
+ s_('ExternalWikiService|External wiki')
end
def description
- s_('ExternalWikiService|Replaces the link to the internal wiki with a link to an external wiki.')
+ s_('ExternalWikiService|Link to an external wiki from the sidebar.')
end
def self.to_param
@@ -22,12 +22,20 @@ class ExternalWikiService < Service
{
type: 'text',
name: 'external_wiki_url',
- placeholder: s_('ExternalWikiService|The URL of the external Wiki'),
+ 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
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 22c2aebaec3..cd49c6d253d 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -39,7 +39,7 @@ class HipchatService < Service
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
{ type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
- { type: 'text', name: 'api_version',
+ { type: 'text', name: 'api_version', title: _('API version'),
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 4a6c8339625..4f1ce16ebb2 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -6,7 +6,7 @@ class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
- validates :recipients, presence: true, if: :valid_recipients?
+ validates :recipients, presence: true, if: :validate_recipients?
before_validation :get_channels
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 694374e9548..19a5b4a74bb 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -73,9 +73,9 @@ class IssueTrackerService < Service
def fields
[
- { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
]
end
diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb
index 63ecfc66877..6a123517b84 100644
--- a/app/models/project_services/jenkins_service.rb
+++ b/app/models/project_services/jenkins_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class JenkinsService < CiService
+ include ActionView::Helpers::UrlHelper
+
prop_accessor :jenkins_url, :project_name, :username, :password
before_update :reset_password
@@ -29,7 +31,6 @@ class JenkinsService < CiService
end
def execute(data)
- return if project.disabled_services.include?(to_param)
return unless supported_events.include?(data[:object_kind])
service_hook.execute(data, "#{data[:object_kind]}_hook")
@@ -59,15 +60,16 @@ class JenkinsService < CiService
end
def title
- 'Jenkins CI'
+ 'Jenkins'
end
def description
- 'An extendable open source continuous integration server'
+ s_('An extendable open source CI/CD server.')
end
def help
- "You must have installed the Git Plugin and GitLab Plugin in Jenkins. [More information](#{Gitlab::Routing.url_helpers.help_page_url('integration/jenkins')})"
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Trigger Jenkins builds when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -77,15 +79,33 @@ class JenkinsService < CiService
def fields
[
{
- type: 'text', name: 'jenkins_url',
- placeholder: 'Jenkins URL like http://jenkins.example.com'
+ 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: 'project_name', placeholder: 'Project Name',
- help: 'The URL-friendly project name. Example: my_project_name'
+ type: 'text',
+ name: 'username',
+ required: true,
+ help: s_('The username for the Jenkins server.')
},
- { type: 'text', name: 'username' },
- { type: 'password', name: 'password' }
+ {
+ 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
index 5857d86f921..3e14bf44c12 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -31,8 +31,8 @@ class JiraService < IssueTrackerService
# 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_id, :project_key, :issues_enabled,
- :vulnerabilities_enabled, :vulnerabilities_issuetype, :proxy_address, :proxy_port, :proxy_username, :proxy_password
+ 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?
@@ -116,7 +116,7 @@ class JiraService < IssueTrackerService
end
def description
- s_('JiraService|Jira issue tracker')
+ s_('JiraService|Track issues in Jira')
end
def self.to_param
@@ -124,15 +124,37 @@ class JiraService < IssueTrackerService
end
def fields
- transition_id_help_path = help_page_path('user/project/integrations/jira', anchor: 'obtaining-a-transition-id')
- transition_id_help_link_start = '<a href="%{transition_id_help_path}" target="_blank" rel="noopener noreferrer">'.html_safe % { transition_id_help_path: transition_id_help_path }
-
[
- { type: 'text', name: 'url', title: s_('JiraService|Web URL'), placeholder: 'https://jira.example.com', required: true },
- { type: 'text', name: 'api_url', title: s_('JiraService|Jira API URL'), placeholder: s_('JiraService|If different from Web URL') },
- { type: 'text', name: 'username', title: s_('JiraService|Username or Email'), placeholder: 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'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true },
- { type: 'text', name: 'jira_issue_transition_id', title: s_('JiraService|Jira workflow transition IDs'), placeholder: s_('JiraService|For example, 12, 24'), help: s_('JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}'.html_safe % { link_start: transition_id_help_link_start, link_end: '</a>'.html_safe }) }
+ {
+ 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
@@ -159,17 +181,19 @@ class JiraService < IssueTrackerService
# support any events.
end
- def find_issue(issue_key, rendered_fields: false)
- options = {}
- options = options.merge(expand: 'renderedFields') if rendered_fields
+ 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) }
+ jira_request { client.Issue.find(issue_key, options || {}) }
end
def close_issue(entity, external_issue, current_user)
- issue = find_issue(external_issue.iid)
+ issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)
- return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
+ return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?
commit_id = case entity
when Commit then entity.id
@@ -244,6 +268,10 @@ class JiraService < IssueTrackerService
true
end
+ def issue_transition_enabled?
+ jira_issue_transition_automatic || jira_issue_transition_id.present?
+ end
+
private
def server_info
@@ -264,20 +292,44 @@ class JiraService < IssueTrackerService
# 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)
- jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).each do |transition_id|
- issue.transitions.build.save!(transition: { id: transition_id })
- rescue => 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
- )
- return false
+ 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 => 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)
diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb
index 6cbcb1550c1..2c145abf5c9 100644
--- a/app/models/project_services/jira_tracker_data.rb
+++ b/app/models/project_services/jira_tracker_data.rb
@@ -2,20 +2,23 @@
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
- attr_encrypted :proxy_address, encryption_options
- attr_encrypted :proxy_port, encryption_options
- attr_encrypted :proxy_username, encryption_options
- attr_encrypted :proxy_password, encryption_options
-
- validates :proxy_address, length: { maximum: 2048 }
- validates :proxy_port, length: { maximum: 5 }
- validates :proxy_username, length: { maximum: 255 }
- validates :proxy_password, length: { maximum: 255 }
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
index 9cff979fcf2..732a7c32a03 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -2,13 +2,14 @@
class MattermostService < ChatNotificationService
include SlackMattermost::Notifier
+ include ActionView::Helpers::UrlHelper
def title
- 'Mattermost notifications'
+ s_('Mattermost notifications')
end
def description
- 'Receive event notifications in Mattermost'
+ s_('Send notifications about project events to Mattermost channels.')
end
def self.to_param
@@ -16,21 +17,15 @@ class MattermostService < ChatNotificationService
end
def help
- 'This service sends notifications about projects events to Mattermost channels.<br />
- To set up this service:
- <ol>
- <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li>
- <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li>
- <li>Paste the webhook <strong>URL</strong> into the field below.</li>
- <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
- </ol>'
+ 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
- "Channel handle (e.g. town-square)"
+ 'my-channel'
end
def webhook_placeholder
- 'http://mattermost.example.com/hooks/…'
+ '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
index f39d3947e5b..60235a09dcd 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -14,7 +14,7 @@ class MattermostSlashCommandsService < SlashCommandsService
end
def description
- "Perform common operations in Mattermost"
+ "Perform common tasks with slash commands."
end
def self.to_param
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index e8e12a9a206..803c1255195 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -2,7 +2,7 @@
class MicrosoftTeamsService < ChatNotificationService
def title
- 'Microsoft Teams Notification'
+ 'Microsoft Teams notifications'
end
def description
@@ -14,13 +14,7 @@ class MicrosoftTeamsService < ChatNotificationService
end
def help
- 'This service sends notifications about projects events to Microsoft Teams channels.<br />
- To set up this service:
- <ol>
- <li><a href="https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors/connectors-using#setting-up-a-custom-incoming-webhook">Setup a custom Incoming Webhook using Office 365 Connectors For Microsoft Teams</a>.</li>
- <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
- <li>Select events below to enable notifications.</li>
- </ol>'
+ '<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
@@ -40,8 +34,8 @@ class MicrosoftTeamsService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { 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
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index c5e5f4f6400..bd6344c6e1a 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -21,10 +21,13 @@ class MockCiService < CiService
def fields
[
- { type: 'text',
+ {
+ type: 'text',
name: 'mock_service_url',
+ title: s_('ProjectService|Mock service URL'),
placeholder: 'http://localhost:4004',
- required: true }
+ required: true
+ }
]
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 8af4cd952c9..0a0a41c525c 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -5,7 +5,7 @@ class PipelinesEmailService < Service
prop_accessor :recipients, :branches_to_be_notified
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
- validates :recipients, presence: true, if: :valid_recipients?
+ validates :recipients, presence: true, if: :validate_recipients?
def initialize_properties
if properties.nil?
@@ -25,11 +25,11 @@ class PipelinesEmailService < Service
end
def title
- _('Pipelines emails')
+ _('Pipeline status emails')
end
def description
- _('Email the pipelines status to a list of recipients.')
+ _('Email the pipeline status to a list of recipients.')
end
def self.to_param
@@ -64,7 +64,7 @@ class PipelinesEmailService < Service
[
{ type: 'textarea',
name: 'recipients',
- placeholder: _('Emails separated by comma'),
+ help: _('Comma-separated list of email addresses.'),
required: true },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' },
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 7324890551c..1781ec7456d 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -20,7 +20,7 @@ class PushoverService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: s_('PushoverService|Your application key'), required: true },
+ { 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:
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index df78520d65f..26a6cf86bf4 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -1,6 +1,7 @@
# 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
@@ -8,7 +9,12 @@ class RedmineService < IssueTrackerService
end
def description
- s_('IssueTracker|Redmine issue tracker')
+ s_('IssueTracker|Use Redmine as the 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
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index f42b3de39d5..7badcc24870 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -16,7 +16,7 @@ class SlackService < ChatNotificationService
end
def description
- 'Receive event notifications in Slack'
+ 'Send notifications about project events to Slack.'
end
def self.to_param
@@ -24,7 +24,7 @@ class SlackService < ChatNotificationService
end
def default_channel_placeholder
- _('Slack channels (e.g. general, development)')
+ _('general, development')
end
def webhook_placeholder
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 209b691ef98..6fc24a4778c 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -51,27 +51,43 @@ class TeamcityService < CiService
end
def title
- 'JetBrains TeamCity CI'
+ 'JetBrains TeamCity'
end
def description
- 'A continuous integration and build server'
+ s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
end
def help
- 'You will want to configure monitoring of all branches so merge '\
- 'requests build, that setting is in the vsc root advanced settings.'
+ 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',
- placeholder: 'TeamCity root URL like https://teamcity.example.com', required: true },
- { type: 'text', name: 'build_type',
- placeholder: 'Build configuration ID', required: true },
- { type: 'text', name: 'username',
- placeholder: 'A user with permissions to trigger a manual build' },
- { type: 'password', name: 'password' }
+ {
+ 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
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 7fb3bde44a5..30abd0159b3 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -26,8 +26,8 @@ class YoutrackService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', title: 'Project URL', placeholder: 'Project URL', required: true },
- { type: 'text', name: 'issues_url', title: 'Issue URL', placeholder: 'Issue URL', required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }
]
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 5b7eded00cd..1a3f362e6a1 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -174,6 +174,10 @@ class ProjectTeam
end
end
+ def write_member_access_for_user_id(user_id, project_access_level)
+ merge_value_to_request_store(User, user_id, project.id, project_access_level)
+ end
+
def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id]
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index cbbdd091feb..963a6b7774a 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -30,7 +30,7 @@ class ProtectedBranch < ApplicationRecord
end
def self.allow_force_push?(project, ref_name)
- return false unless ::Feature.enabled?(:allow_force_push_to_protected_branches, project)
+ 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/raw_usage_data.rb b/app/models/raw_usage_data.rb
index 06cd4ad3f6c..6fe3b26b58b 100644
--- a/app/models/raw_usage_data.rb
+++ b/app/models/raw_usage_data.rb
@@ -4,7 +4,7 @@ class RawUsageData < ApplicationRecord
validates :payload, presence: true
validates :recorded_at, presence: true, uniqueness: true
- def update_sent_at!
- self.update_column(:sent_at, Time.current)
+ def update_version_metadata!(usage_data_id:)
+ self.update_columns(sent_at: Time.current, version_usage_data_id_value: usage_data_id)
end
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 60c2abcacb3..5ca8f537baa 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -8,7 +8,7 @@ class Release < ApplicationRecord
cache_markdown_field :description
- belongs_to :project
+ belongs_to :project, touch: true
# releases prior to 11.7 have no author
belongs_to :author, class_name: 'User'
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 1efba6380e9..98d9899a349 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -3,17 +3,6 @@
class ReleaseHighlight
CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
- RELEASE_VERSIONS_IN_A_YEAR = 12
-
- def self.for_version(version:)
- index = self.versions.index(version)
-
- return if index.nil?
-
- page = index + 1
-
- self.paginated(page: page)
- end
def self.paginated(page: 1)
key = self.cache_key("items:page-#{page}")
@@ -82,15 +71,15 @@ class ReleaseHighlight
end
end
- def self.versions
- key = self.cache_key('versions')
+ def self.most_recent_version_digest
+ key = self.cache_key('most_recent_version_digest')
Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do
- versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
- /\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
- end
+ version = self.paginated&.items&.first&.[]('release')&.to_s
+
+ next if version.nil?
- versions.uniq
+ Digest::SHA256.hexdigest(version)
end
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 880970b72a8..c7387d2197d 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -84,13 +84,7 @@ class RemoteMirror < ApplicationRecord
end
after_transition started: :failed do |remote_mirror|
- Gitlab::Metrics.add_event(:remote_mirrors_failed)
-
- remote_mirror.update(last_update_at: Time.current)
-
- remote_mirror.run_after_commit do
- RemoteMirrorNotificationWorker.perform_async(remote_mirror.id)
- end
+ remote_mirror.send_failure_notifications
end
end
@@ -188,6 +182,24 @@ class RemoteMirror < ApplicationRecord
update_fail!
end
+ # Force the mrror into the retry state
+ def hard_retry!(error_message)
+ update_error_message(error_message)
+ self.update_status = :to_retry
+
+ save!(validate: false)
+ end
+
+ # Force the mirror into the failed state
+ def hard_fail!(error_message)
+ update_error_message(error_message)
+ self.update_status = :failed
+
+ save!(validate: false)
+
+ send_failure_notifications
+ end
+
def url=(value)
super(value) && return unless Gitlab::UrlSanitizer.valid?(value)
@@ -207,7 +219,7 @@ class RemoteMirror < ApplicationRecord
end
def safe_url
- super(usernames_whitelist: %w[git])
+ super(allowed_usernames: %w[git])
end
def bare_url
@@ -239,6 +251,17 @@ class RemoteMirror < ApplicationRecord
last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME
end
+ def send_failure_notifications
+ Gitlab::Metrics.add_event(:remote_mirrors_failed)
+
+ run_after_commit do
+ RemoteMirrorNotificationWorker.perform_async(id)
+ end
+
+ self.last_update_at = Time.current
+ save!(validate: false)
+ end
+
private
def store_credentials
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 84ca8f0c12a..b2efc9b480b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -288,6 +288,10 @@ class Repository
false
end
+ def search_branch_names(pattern)
+ redis_set_cache.search('branch_names', pattern) { branch_names }
+ end
+
def languages
return [] if empty?
@@ -829,12 +833,6 @@ class Repository
end
end
- def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref, allow_conflicts = false)
- branch = merge_request.target_branch
-
- raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref, allow_conflicts)
- end
-
def delete_refs(*ref_names)
raw.delete_refs(*ref_names)
end
@@ -995,6 +993,12 @@ class Repository
raw_repository.search_files_by_name(query, ref)
end
+ def search_files_by_wildcard_path(path, ref = 'HEAD')
+ # We need to use RE2 to match Gitaly's regexp engine
+ regexp_string = RE2::Regexp.escape(path).gsub('\*', '.*?')
+ raw_repository.search_files_by_regexp("^#{regexp_string}$", ref)
+ end
+
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 4165d3b753f..5d7b3879d75 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -48,7 +48,7 @@ class SentNotification < ApplicationRecord
end
def record_note(note, recipient_id, reply_key = self.reply_key, attrs = {})
- attrs[:in_reply_to_discussion_id] = note.discussion_id if note.part_of_discussion?
+ attrs[:in_reply_to_discussion_id] = note.discussion_id if note.part_of_discussion? || note.can_be_discussion_note?
record(note.noteable, recipient_id, reply_key, attrs)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index c49e0869b21..aadc75ae710 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -11,14 +11,14 @@ class Service < ApplicationRecord
include EachBatch
SERVICE_NAMES = %w[
- asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
- drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat hipchat irker jira
+ asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
+ drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
PROJECT_SPECIFIC_SERVICE_NAMES = %w[
- jenkins
+ datadog jenkins
].freeze
# Fake services to help with local development.
@@ -413,6 +413,10 @@ class Service < ApplicationRecord
!instance? && !group_id
end
+ def project_level?
+ project_id.present?
+ end
+
def parent
project || group
end
@@ -456,7 +460,7 @@ class Service < ApplicationRecord
errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id
end
- def valid_recipients?
+ def validate_recipients?
activated? && !importing?
end
end
diff --git a/app/models/sidebars/context.rb b/app/models/sidebars/context.rb
new file mode 100644
index 00000000000..d9ac2705aaf
--- /dev/null
+++ b/app/models/sidebars/context.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# This class stores all the information needed to display and
+# render the sidebar and menus.
+# It usually stores information regarding the context and calculated
+# values where the logic is in helpers.
+module Sidebars
+ class Context
+ attr_reader :current_user, :container
+
+ def initialize(current_user:, container:, **args)
+ @current_user = current_user
+ @container = container
+
+ args.each do |key, value|
+ singleton_class.public_send(:attr_reader, key) # rubocop:disable GitlabSecurity/PublicSend
+ instance_variable_set("@#{key}", value)
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/menu.rb b/app/models/sidebars/menu.rb
new file mode 100644
index 00000000000..a5c8be2bb31
--- /dev/null
+++ b/app/models/sidebars/menu.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module Sidebars
+ class Menu
+ extend ::Gitlab::Utils::Override
+ include ::Gitlab::Routing
+ include GitlabRoutingHelper
+ include Gitlab::Allowable
+ include ::Sidebars::HasPill
+ include ::Sidebars::HasIcon
+ include ::Sidebars::PositionableList
+ include ::Sidebars::Renderable
+ include ::Sidebars::ContainerWithHtmlOptions
+ include ::Sidebars::HasActiveRoutes
+
+ attr_reader :context
+ delegate :current_user, :container, to: :@context
+
+ def initialize(context)
+ @context = context
+ @items = []
+
+ configure_menu_items
+ end
+
+ def configure_menu_items
+ # No-op
+ end
+
+ override :render?
+ def render?
+ @items.empty? || renderable_items.any?
+ end
+
+ # Menus might have or not a link
+ override :link
+ def link
+ nil
+ end
+
+ # This method normalizes the information retrieved from the submenus and this menu
+ # Value from menus is something like: [{ path: 'foo', path: 'bar', controller: :foo }]
+ # This method filters the information and returns: { path: ['foo', 'bar'], controller: :foo }
+ def all_active_routes
+ @all_active_routes ||= begin
+ ([active_routes] + renderable_items.map(&:active_routes)).flatten.each_with_object({}) do |pairs, hash|
+ pairs.each do |k, v|
+ hash[k] ||= []
+ hash[k] += Array(v)
+ hash[k].uniq!
+ end
+
+ hash
+ end
+ end
+ end
+
+ def has_items?
+ @items.any?
+ end
+
+ def add_item(item)
+ add_element(@items, item)
+ end
+
+ def insert_item_before(before_item, new_item)
+ insert_element_before(@items, before_item, new_item)
+ end
+
+ def insert_item_after(after_item, new_item)
+ insert_element_after(@items, after_item, new_item)
+ end
+
+ def has_renderable_items?
+ renderable_items.any?
+ end
+
+ def renderable_items
+ @renderable_items ||= @items.select(&:render?)
+ end
+ end
+end
diff --git a/app/models/sidebars/menu_item.rb b/app/models/sidebars/menu_item.rb
new file mode 100644
index 00000000000..7466b31898e
--- /dev/null
+++ b/app/models/sidebars/menu_item.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Sidebars
+ class MenuItem
+ extend ::Gitlab::Utils::Override
+ include ::Gitlab::Routing
+ include GitlabRoutingHelper
+ include Gitlab::Allowable
+ include ::Sidebars::HasIcon
+ include ::Sidebars::HasHint
+ include ::Sidebars::Renderable
+ include ::Sidebars::ContainerWithHtmlOptions
+ include ::Sidebars::HasActiveRoutes
+
+ attr_reader :context
+
+ def initialize(context)
+ @context = context
+ end
+ end
+end
diff --git a/app/models/sidebars/panel.rb b/app/models/sidebars/panel.rb
new file mode 100644
index 00000000000..5c8191ebda3
--- /dev/null
+++ b/app/models/sidebars/panel.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Sidebars
+ class Panel
+ extend ::Gitlab::Utils::Override
+ include ::Sidebars::PositionableList
+
+ attr_reader :context, :scope_menu, :hidden_menu
+
+ def initialize(context)
+ @context = context
+ @scope_menu = nil
+ @hidden_menu = nil
+ @menus = []
+
+ configure_menus
+ end
+
+ def configure_menus
+ # No-op
+ end
+
+ def add_menu(menu)
+ add_element(@menus, menu)
+ end
+
+ def insert_menu_before(before_menu, new_menu)
+ insert_element_before(@menus, before_menu, new_menu)
+ end
+
+ def insert_menu_after(after_menu, new_menu)
+ insert_element_after(@menus, after_menu, new_menu)
+ end
+
+ def set_scope_menu(scope_menu)
+ @scope_menu = scope_menu
+ end
+
+ def set_hidden_menu(hidden_menu)
+ @hidden_menu = hidden_menu
+ end
+
+ def aria_label
+ raise NotImplementedError
+ end
+
+ def has_renderable_menus?
+ renderable_menus.any?
+ end
+
+ def renderable_menus
+ @renderable_menus ||= @menus.select(&:render?)
+ end
+
+ def container
+ context.container
+ end
+
+ # Auxiliar method that helps with the migration from
+ # regular views to the new logic
+ def render_raw_scope_menu_partial
+ # No-op
+ end
+
+ # Auxiliar method that helps with the migration from
+ # regular views to the new logic.
+ #
+ # Any menu inside this partial will be added after
+ # all the menus added in the `configure_menus`
+ # method.
+ def render_raw_menus_partial
+ # No-op
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/context.rb b/app/models/sidebars/projects/context.rb
new file mode 100644
index 00000000000..4c82309035d
--- /dev/null
+++ b/app/models/sidebars/projects/context.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ class Context < ::Sidebars::Context
+ def initialize(current_user:, container:, **args)
+ super(current_user: current_user, container: container, project: container, **args)
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/learn_gitlab/menu.rb b/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
new file mode 100644
index 00000000000..4b572846d1a
--- /dev/null
+++ b/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module LearnGitlab
+ class Menu < ::Sidebars::Menu
+ override :link
+ def link
+ project_learn_gitlab_path(context.project)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :learn_gitlab }
+ end
+
+ override :title
+ def title
+ _('Learn GitLab')
+ end
+
+ override :extra_container_html_options
+ def nav_link_html_options
+ { class: 'home' }
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'home'
+ end
+
+ override :render?
+ def render?
+ context.learn_gitlab_experiment_enabled
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu.rb b/app/models/sidebars/projects/menus/project_overview/menu.rb
new file mode 100644
index 00000000000..e6aa8ed159f
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ class Menu < ::Sidebars::Menu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(MenuItems::Details.new(context))
+ add_item(MenuItems::Activity.new(context))
+ add_item(MenuItems::Releases.new(context))
+ end
+
+ override :link
+ def link
+ project_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-project rspec-project-link'
+ }
+ end
+
+ override :extra_container_html_options
+ def nav_link_html_options
+ { class: 'home' }
+ end
+
+ override :title
+ def title
+ _('Project overview')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'home'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
new file mode 100644
index 00000000000..46d0f0bc43b
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ module MenuItems
+ class Activity < ::Sidebars::MenuItem
+ override :link
+ def link
+ activity_project_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-project-activity'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { path: 'projects#activity' }
+ end
+
+ override :title
+ def title
+ _('Activity')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
new file mode 100644
index 00000000000..c40c2ed8fa2
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ module MenuItems
+ class Details < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ aria: { label: _('Project details') },
+ class: 'shortcuts-project'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { path: 'projects#show' }
+ end
+
+ override :title
+ def title
+ _('Details')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
new file mode 100644
index 00000000000..5e8348f4398
--- /dev/null
+++ b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module ProjectOverview
+ module MenuItems
+ class Releases < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_releases_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-project-releases'
+ }
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :read_release, context.project) && !context.project.empty_repo?
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :releases }
+ end
+
+ override :title
+ def title
+ _('Releases')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu.rb b/app/models/sidebars/projects/menus/repository/menu.rb
new file mode 100644
index 00000000000..f49a0479521
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ class Menu < ::Sidebars::Menu
+ override :configure_menu_items
+ def configure_menu_items
+ add_item(MenuItems::Files.new(context))
+ add_item(MenuItems::Commits.new(context))
+ add_item(MenuItems::Branches.new(context))
+ add_item(MenuItems::Tags.new(context))
+ add_item(MenuItems::Contributors.new(context))
+ add_item(MenuItems::Graphs.new(context))
+ add_item(MenuItems::Compare.new(context))
+ end
+
+ override :link
+ def link
+ project_tree_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-tree'
+ }
+ end
+
+ override :title
+ def title
+ _('Repository')
+ end
+
+ override :title_html_options
+ def title_html_options
+ {
+ id: 'js-onboarding-repo-link'
+ }
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'doc-text'
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :download_code, context.project) &&
+ !context.project.empty_repo?
+ end
+ end
+ end
+ end
+ end
+end
+
+Sidebars::Projects::Menus::Repository::Menu.prepend_if_ee('EE::Sidebars::Projects::Menus::Repository::Menu')
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/branches.rb b/app/models/sidebars/projects/menus/repository/menu_items/branches.rb
new file mode 100644
index 00000000000..4a62803dd2b
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/branches.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Branches < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_branches_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ id: 'js-onboarding-branches-link'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :branches }
+ end
+
+ override :title
+ def title
+ _('Branches')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/commits.rb b/app/models/sidebars/projects/menus/repository/menu_items/commits.rb
new file mode 100644
index 00000000000..647cf89133e
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/commits.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Commits < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_commits_path(context.project, context.current_ref)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ id: 'js-onboarding-commits-link'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: %w(commit commits) }
+ end
+
+ override :title
+ def title
+ _('Commits')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/compare.rb b/app/models/sidebars/projects/menus/repository/menu_items/compare.rb
new file mode 100644
index 00000000000..4812636b63f
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/compare.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Compare < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_compare_index_path(context.project, from: context.project.repository.root_ref, to: context.current_ref)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :compare }
+ end
+
+ override :title
+ def title
+ _('Compare')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb b/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb
new file mode 100644
index 00000000000..d60fd05bb64
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Contributors < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_graph_path(context.project, context.current_ref)
+ end
+
+ override :active_routes
+ def active_routes
+ { path: 'graphs#show' }
+ end
+
+ override :title
+ def title
+ _('Contributors')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/files.rb b/app/models/sidebars/projects/menus/repository/menu_items/files.rb
new file mode 100644
index 00000000000..4989efe9fa5
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/files.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Files < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_tree_path(context.project, context.current_ref)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: %w[tree blob blame edit_tree new_tree find_file] }
+ end
+
+ override :title
+ def title
+ _('Files')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb b/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb
new file mode 100644
index 00000000000..a57021be4d0
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Graphs < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_network_path(context.project, context.current_ref)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :network }
+ end
+
+ override :title
+ def title
+ _('Graph')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/tags.rb b/app/models/sidebars/projects/menus/repository/menu_items/tags.rb
new file mode 100644
index 00000000000..d84bc89b93c
--- /dev/null
+++ b/app/models/sidebars/projects/menus/repository/menu_items/tags.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Repository
+ module MenuItems
+ class Tags < ::Sidebars::MenuItem
+ override :link
+ def link
+ project_tags_path(context.project)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :tags }
+ end
+
+ override :title
+ def title
+ _('Tags')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/menus/scope/menu.rb b/app/models/sidebars/projects/menus/scope/menu.rb
new file mode 100644
index 00000000000..3b699083f75
--- /dev/null
+++ b/app/models/sidebars/projects/menus/scope/menu.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module Scope
+ class Menu < ::Sidebars::Menu
+ override :link
+ def link
+ project_path(context.project)
+ end
+
+ override :title
+ def title
+ context.project.name
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb
new file mode 100644
index 00000000000..ec4fac53a40
--- /dev/null
+++ b/app/models/sidebars/projects/panel.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ class Panel < ::Sidebars::Panel
+ override :configure_menus
+ def configure_menus
+ set_scope_menu(Sidebars::Projects::Menus::Scope::Menu.new(context))
+
+ add_menu(Sidebars::Projects::Menus::ProjectOverview::Menu.new(context))
+ add_menu(Sidebars::Projects::Menus::LearnGitlab::Menu.new(context))
+ add_menu(Sidebars::Projects::Menus::Repository::Menu.new(context))
+ end
+
+ override :render_raw_menus_partial
+ def render_raw_menus_partial
+ 'layouts/nav/sidebar/project_menus'
+ end
+
+ override :aria_label
+ def aria_label
+ _('Project navigation')
+ end
+ end
+ end
+end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index f4debedb656..c1aa84cbbcd 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -31,9 +31,9 @@ class Timelog < ApplicationRecord
def issuable_id_is_present
if issue_id && merge_request_id
- errors.add(:base, _('Only Issue ID or Merge Request ID is required'))
+ errors.add(:base, _('Only Issue ID or merge request ID is required'))
elsif issuable.nil?
- errors.add(:base, _('Issue or Merge Request ID is required'))
+ errors.add(:base, _('Issue or merge request ID is required'))
end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 176d5e56fc0..c8138587d83 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -148,6 +148,24 @@ class Todo < ApplicationRecord
.order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
.order('todos.created_at')
end
+
+ def pluck_user_id
+ pluck(:user_id)
+ end
+
+ # Count todos grouped by user_id and state, using an UNION query
+ # so we can utilize the partial indexes for each state.
+ def count_grouped_by_user_id_and_state
+ grouped_count = select(:user_id, 'count(id) AS count').group(:user_id)
+
+ done = grouped_count.where(state: :done).select("'done' AS state")
+ pending = grouped_count.where(state: :pending).select("'pending' AS state")
+ union = unscoped.from_union([done, pending], remove_duplicates: false)
+
+ connection.select_all(union).each_with_object({}) do |row, counts|
+ counts[[row['user_id'], row['state']]] = row['count']
+ end
+ end
end
def resource_parent
diff --git a/app/models/user.rb b/app/models/user.rb
index 11046bdabe4..507e8cc2cf5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -103,6 +103,8 @@ 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 :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
has_many :gpg_keys
@@ -125,7 +127,7 @@ class User < ApplicationRecord
# Groups
has_many :members
- has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember'
+ has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, class_name: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
@@ -139,7 +141,7 @@ class User < ApplicationRecord
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
source: :group
- has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
+ has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, class_name: 'GroupMember'
has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
# Projects
@@ -199,6 +201,8 @@ class User < ApplicationRecord
has_many :reviews, foreign_key: :author_id, inverse_of: :author
+ has_many :in_product_marketing_emails, class_name: '::Users::InProductMarketingEmail'
+
#
# Validations
#
@@ -350,7 +354,8 @@ class User < ApplicationRecord
# this state transition object in order to do a rollback.
# For this reason the tradeoff is to disable this cop.
after_transition any => :blocked do |user|
- Ci::CancelUserPipelinesService.new.execute(user)
+ Ci::DropPipelineService.new.execute_async_for_all(user.pipelines, :user_blocked, user)
+ Ci::DisableUserPipelineSchedulesService.new.execute(user)
end
# rubocop: enable CodeReuse/ServiceClass
end
@@ -390,6 +395,22 @@ 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 (?)',
+ ::Key
+ .select(1)
+ .where('keys.user_id = users.id')
+ .expiring_soon_and_not_notified)
+ end
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
@@ -743,6 +764,7 @@ class User < ApplicationRecord
u.bio = 'The GitLab support bot used for Service Desk'
u.name = 'GitLab Support Bot'
u.avatar = bot_avatar(image: 'support-bot.png')
+ u.confirmed_at = Time.zone.now
end
end
@@ -1024,7 +1046,7 @@ class User < ApplicationRecord
[
Project.where(namespace: namespace),
Project.joins(:project_authorizations)
- .where("projects.namespace_id <> ?", namespace.id)
+ .where.not('projects.namespace_id' => namespace.id)
.where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
],
remove_duplicates: false
@@ -1337,9 +1359,11 @@ class User < ApplicationRecord
end
def public_verified_emails
- emails = verified_emails(include_private_email: false)
- emails << email unless temp_oauth_email?
- emails.uniq
+ strong_memoize(:public_verified_emails) do
+ emails = verified_emails(include_private_email: false)
+ emails << email unless temp_oauth_email?
+ emails.uniq
+ end
end
def any_email?(check_email)
@@ -1595,32 +1619,40 @@ class User < ApplicationRecord
@global_notification_setting
end
+ def count_cache_validity_period
+ if Feature.enabled?(:longer_count_cache_validity, self, default_enabled: :yaml)
+ 24.hours
+ else
+ 20.minutes
+ end
+ end
+
def assigned_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: count_cache_validity_period) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def review_requested_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
+ Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: count_cache_validity_period) do
MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count
end
end
def assigned_open_issues_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
+ Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: count_cache_validity_period) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def todos_done_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: count_cache_validity_period) do
TodosFinder.new(self, state: :done).execute.count
end
end
def todos_pending_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: count_cache_validity_period) do
TodosFinder.new(self, state: :pending).execute.count
end
end
@@ -1639,8 +1671,7 @@ class User < ApplicationRecord
def invalidate_cache_counts
invalidate_issue_cache_counts
invalidate_merge_request_cache_counts
- invalidate_todos_done_count
- invalidate_todos_pending_count
+ invalidate_todos_cache_counts
invalidate_personal_projects_count
end
@@ -1653,11 +1684,8 @@ class User < ApplicationRecord
Rails.cache.delete(['users', id, 'review_requested_open_merge_requests_count'])
end
- def invalidate_todos_done_count
+ def invalidate_todos_cache_counts
Rails.cache.delete(['users', id, 'todos_done_count'])
- end
-
- def invalidate_todos_pending_count
Rails.cache.delete(['users', id, 'todos_pending_count'])
end
@@ -1835,10 +1863,12 @@ class User < ApplicationRecord
end
def dismissed_callout?(feature_name:, ignore_dismissal_earlier_than: nil)
- callouts = self.callouts.with_feature_name(feature_name)
- callouts = callouts.with_dismissed_after(ignore_dismissal_earlier_than) if ignore_dismissal_earlier_than
+ callout = callouts_by_feature_name[feature_name]
+
+ return false unless callout
+ return callout.dismissed_after?(ignore_dismissal_earlier_than) if ignore_dismissal_earlier_than
- callouts.any?
+ true
end
# Load the current highest access by looking directly at the user's memberships
@@ -1901,6 +1931,10 @@ class User < ApplicationRecord
private
+ def callouts_by_feature_name
+ @callouts_by_feature_name ||= callouts.index_by(&:feature_name)
+ end
+
def authorized_groups_without_shared_membership
Group.from_union([
groups,
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index bb5a9dceaeb..0a4db707be6 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -17,7 +17,7 @@ class UserCallout < ApplicationRecord
threat_monitoring_info: 11, # EE-only
account_recovery_regular_check: 12, # EE-only
webhooks_moved: 13,
- service_templates_deprecated: 14,
+ 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
@@ -29,7 +29,8 @@ class UserCallout < ApplicationRecord
registration_enabled_callout: 25,
new_user_signups_cap_reached: 26, # EE-only
unfinished_tag_cleanup_callout: 27,
- eoa_bronze_plan_banner: 28 # EE-only
+ eoa_bronze_plan_banner: 28, # EE-only
+ pipeline_needs_banner: 29
}
validates :user, presence: true
@@ -38,6 +39,7 @@ class UserCallout < ApplicationRecord
uniqueness: { scope: :user_id },
inclusion: { in: UserCallout.feature_names.keys }
- scope :with_feature_name, -> (feature_name) { where(feature_name: UserCallout.feature_names[feature_name]) }
- scope :with_dismissed_after, -> (dismissed_after) { where('dismissed_at > ?', dismissed_after) }
+ def dismissed_after?(dismissed_after)
+ dismissed_at > dismissed_after
+ end
end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index ef799b01452..6b64f583927 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -19,7 +19,7 @@ class UserDetail < ApplicationRecord
# For backward compatibility.
# Older migrations (and their tests) reference the `User.migration_bot` where the `bio` attribute is set.
- # Here we disable writing the markdown cache when the `bio_html` column does not exists.
+ # Here we disable writing the markdown cache when the `bio_html` column does not exist.
override :invalidated_markdown_cache?
def invalidated_markdown_cache?
self.class.column_names.include?('bio_html') && super
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
new file mode 100644
index 00000000000..195cfe162ac
--- /dev/null
+++ b/app/models/users/in_product_marketing_email.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Users
+ class InProductMarketingEmail < ApplicationRecord
+ include BulkInsertSafe
+
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :track, presence: true
+ validates :series, presence: true
+ validates :user_id, uniqueness: {
+ scope: [:track, :series],
+ message: 'has already been sent'
+ }
+
+ enum track: {
+ create: 0,
+ verify: 1,
+ trial: 2,
+ team: 3
+ }, _suffix: true
+
+ scope :without_track_and_series, -> (track, series) do
+ users = User.arel_table
+ product_emails = arel_table
+
+ join_condition = users[:id].eq(product_emails[:user_id])
+ .and(product_emails[:track]).eq(tracks[track])
+ .and(product_emails[:series]).eq(series)
+
+ arel_join = users.join(product_emails, Arel::Nodes::OuterJoin).on(join_condition)
+
+ joins(arel_join.join_sources)
+ .where(in_product_marketing_emails: { id: nil })
+ .select(Arel.sql("DISTINCT ON(#{users.table_name}.id) #{users.table_name}.*"))
+ end
+
+ scope :for_user_with_track_and_series, -> (user, track, series) do
+ where(user: user, track: track, series: series)
+ end
+
+ def self.save_cta_click(user, track, series)
+ email = for_user_with_track_and_series(user, track, series).take
+
+ email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
+ end
+ end
+end
diff --git a/app/models/users/merge_request_interaction.rb b/app/models/users/merge_request_interaction.rb
new file mode 100644
index 00000000000..35d1d3206b5
--- /dev/null
+++ b/app/models/users/merge_request_interaction.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Users
+ class MergeRequestInteraction
+ def initialize(user:, merge_request:)
+ @user = user
+ @merge_request = merge_request
+ end
+
+ def declarative_policy_subject
+ merge_request
+ end
+
+ def can_merge?
+ merge_request.can_be_merged_by?(user)
+ end
+
+ def can_update?
+ user.can?(:update_merge_request, merge_request)
+ end
+
+ def review_state
+ reviewer&.state
+ end
+
+ def reviewed?
+ reviewer&.reviewed? == true
+ end
+
+ def approved?
+ merge_request.approvals.any? { |app| app.user_id == user.id }
+ end
+
+ private
+
+ def reviewer
+ @reviewer ||= merge_request.merge_request_reviewers.find { |r| r.user_id == user.id }
+ end
+
+ attr_reader :user, :merge_request
+ end
+end
+
+::Users::MergeRequestInteraction.prepend_if_ee('EE::Users::MergeRequestInteraction')
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index df31c54bd0f..47fe40b0e57 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -160,16 +160,12 @@ class Wiki
end
def find_file(name, version = 'HEAD', load_content: true)
- if Feature.enabled?(:gitaly_find_file, user, default_enabled: :yaml)
- data_limit = load_content ? -1 : 0
- blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)
+ data_limit = load_content ? -1 : 0
+ blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)
- return if blobs.empty?
+ return if blobs.empty?
- Gitlab::Git::WikiFile.from_blob(blobs.first)
- else
- wiki.file(name, version)
- end
+ Gitlab::Git::WikiFile.new(blobs.first)
end
def create_page(title, content, format = :markdown, message = nil)
@@ -196,10 +192,20 @@ class Wiki
def delete_page(page, message = nil)
return unless page
- wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
- after_wiki_activity
+ if Feature.enabled?(:gitaly_replace_wiki_delete_page, user, default_enabled: :yaml)
+ capture_git_error(:deleted) do
+ repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
- true
+ after_wiki_activity
+
+ true
+ end
+ else
+ wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
+ after_wiki_activity
+
+ true
+ end
end
def page_title_and_dir(title)
@@ -276,8 +282,20 @@ class Wiki
private
+ def multi_commit_options(action, message = nil, title = nil)
+ commit_message = build_commit_message(action, message, title)
+ git_user = Gitlab::Git::User.from_gitlab(user)
+
+ {
+ branch_name: repository.root_ref,
+ message: commit_message,
+ author_email: git_user.email,
+ author_name: git_user.name
+ }
+ end
+
def commit_details(action, message = nil, title = nil)
- commit_message = message.presence || default_message(action, title)
+ commit_message = build_commit_message(action, message, title)
git_user = Gitlab::Git::User.from_gitlab(user)
Gitlab::Git::Wiki::CommitDetails.new(user.id,
@@ -287,9 +305,26 @@ class Wiki
commit_message)
end
+ def build_commit_message(action, message, title)
+ message.presence || default_message(action, title)
+ end
+
def default_message(action, title)
"#{user.username} #{action} page: #{title}"
end
+
+ def capture_git_error(action, &block)
+ yield block
+ rescue Gitlab::Git::Index::IndexError,
+ Gitlab::Git::CommitError,
+ Gitlab::Git::PreReceiveError,
+ Gitlab::Git::CommandError,
+ ArgumentError => error
+
+ Gitlab::ErrorTracking.log_exception(error, action: action, wiki_id: id)
+
+ false
+ end
end
Wiki.prepend_if_ee('EE::Wiki')