diff options
Diffstat (limited to 'app/models')
28 files changed, 301 insertions, 117 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bc31e548a09..bbe7811841a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -230,25 +230,27 @@ class ApplicationSetting < ActiveRecord::Base { after_sign_up_text: nil, akismet_enabled: false, + allow_local_requests_from_hooks_and_services: false, authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], + default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], - default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], disabled_oauth_sign_in_sources: [], domain_whitelist: Settings.gitlab['domain_whitelist'], dsa_key_restriction: 0, ecdsa_key_restriction: 0, ed25519_key_restriction: 0, + gitaly_timeout_default: 55, + gitaly_timeout_fast: 10, + gitaly_timeout_medium: 30, gravatar_enabled: Settings.gravatar['enabled'], - help_page_text: nil, help_page_hide_commercial_content: false, - unique_ips_limit_per_user: 10, - unique_ips_limit_time_window: 3600, - unique_ips_limit_enabled: false, + help_page_text: nil, + hide_third_party_offers: false, housekeeping_bitmaps_enabled: true, housekeeping_enabled: true, housekeeping_full_repack_period: 50, @@ -259,12 +261,14 @@ class ApplicationSetting < ActiveRecord::Base koding_url: nil, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], - password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], + mirror_available: true, password_authentication_enabled_for_git: true, + password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], performance_bar_allowed_group_id: nil, rsa_key_restriction: 0, plantuml_enabled: false, plantuml_url: nil, + polling_interval_multiplier: 1, project_export_enabled: true, recaptcha_enabled: false, repository_checks_enabled: true, @@ -279,25 +283,22 @@ class ApplicationSetting < ActiveRecord::Base sign_in_text: nil, signup_enabled: Settings.gitlab['signup_enabled'], terminal_max_session_time: 0, - throttle_unauthenticated_enabled: false, - throttle_unauthenticated_requests_per_period: 3600, - throttle_unauthenticated_period_in_seconds: 3600, - throttle_authenticated_web_enabled: false, - throttle_authenticated_web_requests_per_period: 7200, - throttle_authenticated_web_period_in_seconds: 3600, throttle_authenticated_api_enabled: false, - throttle_authenticated_api_requests_per_period: 7200, throttle_authenticated_api_period_in_seconds: 3600, + throttle_authenticated_api_requests_per_period: 7200, + throttle_authenticated_web_enabled: false, + throttle_authenticated_web_period_in_seconds: 3600, + throttle_authenticated_web_requests_per_period: 7200, + throttle_unauthenticated_enabled: false, + throttle_unauthenticated_period_in_seconds: 3600, + throttle_unauthenticated_requests_per_period: 3600, two_factor_grace_period: 48, - user_default_external: false, - polling_interval_multiplier: 1, + unique_ips_limit_enabled: false, + unique_ips_limit_per_user: 10, + unique_ips_limit_time_window: 3600, usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], - gitaly_timeout_fast: 10, - gitaly_timeout_medium: 30, - gitaly_timeout_default: 55, - allow_local_requests_from_hooks_and_services: false, - hide_third_party_offers: false, - mirror_available: true + instance_statistics_visibility_private: false, + user_default_external: false } end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index db86400128c..93bbee49c09 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -8,8 +8,6 @@ module Ci include Importable include Gitlab::Utils::StrongMemoize - MissingDependenciesError = Class.new(StandardError) - belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request @@ -17,14 +15,19 @@ module Ci has_many :deployments, as: :deployable + RUNNER_FEATURES = { + upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? } + }.freeze + has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent - has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id - has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id - has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id + + Ci::JobArtifact.file_types.each do |key, value| + has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id + end has_one :metadata, class_name: 'Ci::BuildMetadata' has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build @@ -173,10 +176,6 @@ module Ci end end - before_transition any => [:running] do |build| - build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies') - end - after_transition pending: :running do |build| build.ensure_metadata.update_timeout_state end @@ -386,6 +385,10 @@ module Ci trace.exist? end + def has_test_reports? + job_artifacts.test_reports.any? + end + def has_old_trace? old_trace.present? end @@ -453,16 +456,22 @@ module Ci save end + def erase_test_reports! + # TODO: Use fast_destroy_all in the context of https://gitlab.com/gitlab-org/gitlab-ce/issues/35240 + job_artifacts_junit&.destroy + end + def erase(opts = {}) return false unless erasable? erase_artifacts! + erase_test_reports! erase_trace! update_erased!(opts[:erased_by]) end def erasable? - complete? && (artifacts? || has_trace?) + complete? && (artifacts? || has_test_reports? || has_trace?) end def erased? @@ -539,10 +548,6 @@ module Ci Gitlab::Ci::Build::Image.from_services(self) end - def artifacts - [options[:artifacts]] - end - def cache cache = options[:cache] @@ -574,10 +579,10 @@ module Ci options[:dependencies]&.empty? end - def validates_dependencies! - dependencies.each do |dependency| - raise MissingDependenciesError unless dependency.valid_dependency? - end + def has_valid_build_dependencies? + return true if Feature.enabled?('ci_disable_validates_dependencies') + + dependencies.all?(&:valid_dependency?) end def valid_dependency? @@ -587,6 +592,24 @@ module Ci true end + def runner_required_feature_names + strong_memoize(:runner_required_feature_names) do + RUNNER_FEATURES.select do |feature, method| + method.call(self) + end.keys + end + end + + def supported_runner?(features) + runner_required_feature_names.all? do |feature_name| + features&.dig(feature_name) + end + end + + def publishes_artifacts_reports? + options&.dig(:artifacts, :reports)&.any? + end + def hide_secrets(trace) return unless trace diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 6f3be31d8e1..869dc0ccadf 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -17,7 +17,7 @@ module Ci { subprotocols: ['terminal.gitlab.com'].freeze, url: "#{url}/exec".sub("https://", "wss://"), - headers: { Authorization: authorization.presence }.compact, + headers: { Authorization: [authorization.presence] }.compact, ca_pem: certificate.presence } end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 3b952391b7e..054b714f8ac 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -4,11 +4,17 @@ module Ci include ObjectStorage::BackgroundMove extend Gitlab::Ci::Model + TEST_REPORT_FILE_TYPES = %w[junit].freeze + DEFAULT_FILE_NAMES = { junit: 'junit.xml' }.freeze + TYPE_AND_FORMAT_PAIRS = { archive: :zip, metadata: :gzip, trace: :raw, junit: :gzip }.freeze + belongs_to :project belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id mount_uploader :file, JobArtifactUploader + validates :file_format, presence: true, unless: :trace?, on: :create + validate :valid_file_format?, unless: :trace?, on: :create before_save :set_size, if: :file_changed? after_save :update_project_statistics_after_save, if: :size_changed? after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed? @@ -17,14 +23,33 @@ module Ci scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } + scope :test_reports, -> do + types = self.file_types.select { |file_type| TEST_REPORT_FILE_TYPES.include?(file_type) }.values + + where(file_type: types) + end + delegate :exists?, :open, to: :file enum file_type: { archive: 1, metadata: 2, - trace: 3 + trace: 3, + junit: 4 } + enum file_format: { + raw: 1, + zip: 2, + gzip: 3 + } + + def valid_file_format? + unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym + errors.add(:file_format, 'Invalid file format with specified file type') + end + end + def update_file_store # The file.object_store is set during `uploader.store!` # which happens after object is inserted/updated diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4163fe31477..b65d7672973 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -48,7 +48,8 @@ class CommitStatus < ActiveRecord::Base api_failure: 2, stuck_or_timeout_failure: 3, runner_system_failure: 4, - missing_dependency_failure: 5 + missing_dependency_failure: 5, + runner_unsupported: 6 } ## diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 164c704260e..5e39676b24b 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -26,21 +26,30 @@ module AtomicInternalId module ClassMethods def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName + # We require init here to retain the ability to recalculate in the absence of a + # InternaLId record (we may delete records in `internal_ids` for example). + raise "has_internal_id requires a init block, none given." unless init + before_validation :"ensure_#{scope}_#{column}!", on: :create validates column, presence: presence define_method("ensure_#{scope}_#{column}!") do scope_value = association(scope).reader + value = read_attribute(column) + + return value unless scope_value - if read_attribute(column).blank? && scope_value - scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } - usage = self.class.table_name.to_sym + scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } + usage = self.class.table_name.to_sym - new_iid = InternalId.generate_next(self, scope_attrs, usage, init) - write_attribute(column, new_iid) + if value.present? + InternalId.track_greatest(self, scope_attrs, usage, value, init) + else + value = InternalId.generate_next(self, scope_attrs, usage, init) + write_attribute(column, value) end - read_attribute(column) + value end end end diff --git a/app/models/concerns/label_eventable.rb b/app/models/concerns/label_eventable.rb new file mode 100644 index 00000000000..d22d93448e4 --- /dev/null +++ b/app/models/concerns/label_eventable.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# == LabelEventable concern +# +# Contains functionality related to objects that support adding/removing labels. +# +# This concern is not used yet, it will be used for: +# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 + +module LabelEventable + extend ActiveSupport::Concern + + included do + has_many :resource_label_events + end +end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 0176a12a131..cb91f8fbac8 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -90,34 +90,17 @@ module Routable end def full_name - if route && route.name.present? - @full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables - else - update_route if persisted? - - build_full_name - end + route&.name || build_full_name end - # Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path, - # a new instance is instantiated, and we end up duplicating the same query to retrieve - # the route. Caching this per request ensures that even if we have multiple instances, - # we will not have to duplicate work, avoiding N+1 queries in some cases. def full_path - return uncached_full_path unless RequestStore.active? && persisted? - - RequestStore[full_path_key] ||= uncached_full_path + route&.path || build_full_path end def full_path_components full_path.split('/') end - def expires_full_path_cache - RequestStore.delete(full_path_key) if RequestStore.active? - @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables - end - def build_full_path if parent && path parent.full_path + '/' + path @@ -138,16 +121,6 @@ module Routable self.errors[:path].concat(route_path_errors) if route_path_errors end - def uncached_full_path - if route && route.path.present? - @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables - else - update_route if persisted? - - build_full_path - end - end - def full_name_changed? name_changed? || parent_changed? end @@ -156,10 +129,6 @@ module Routable path_changed? || parent_changed? end - def full_path_key - @full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}" - end - def build_full_name if parent && name parent.human_name + ' / ' + name @@ -168,18 +137,9 @@ module Routable end end - def update_route - return if Gitlab::Database.read_only? - - prepare_route - route.save - end - def prepare_route route || build_route(source: self) route.path = build_full_path route.name = build_full_name - @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables - @full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index ae6595320cb..f5225cd81ed 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -11,8 +11,6 @@ module Storage Namespace.find(parent_id_was) # raise NotFound early if needed end - expires_full_path_cache - move_repositories if parent_changed? diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 446c4576678..c466304a827 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -3,6 +3,7 @@ class DeployToken < ActiveRecord::Base include Expirable include TokenAuthenticatable + include PolicyActor add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze @@ -60,10 +61,6 @@ class DeployToken < ActiveRecord::Base write_attribute(:expires_at, value.presence || Forever.date) end - def admin? - false - end - private def ensure_at_least_one_scope diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 6a3714f1f10..4eb211eff61 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -3,6 +3,9 @@ # An InternalId is a strictly monotone sequence of integers # generated for a given scope and usage. # +# The monotone sequence may be broken if an ID is explicitly provided +# to `.track_greatest_and_save!` or `#track_greatest`. +# # For example, issues use their project to scope internal ids: # In that sense, scope is "project" and usage is "issues". # Generated internal ids for an issue are unique per project. @@ -27,13 +30,34 @@ class InternalId < ActiveRecord::Base # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # As such, the increment is atomic and safe to be called concurrently. def increment_and_save! + update_and_save { self.last_value = (last_value || 0) + 1 } + end + + # Increments #last_value with new_value if it is greater than the current, + # and saves the record + # + # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). + # As such, the increment is atomic and safe to be called concurrently. + def track_greatest_and_save!(new_value) + update_and_save { self.last_value = [last_value || 0, new_value].max } + end + + private + + def update_and_save(&block) lock! - self.last_value = (last_value || 0) + 1 + yield save! last_value end class << self + def track_greatest(subject, scope, usage, new_value, init) + return new_value unless available? + + InternalIdGenerator.new(subject, scope, usage, init).track_greatest(new_value) + end + def generate_next(subject, scope, usage, init) # Shortcut if `internal_ids` table is not available (yet) # This can be the case in other (unrelated) migration specs @@ -96,6 +120,16 @@ class InternalId < ActiveRecord::Base end end + # Create a record in internal_ids if one does not yet exist + # and set its new_value if it is higher than the current last_value + # + # Note this will acquire a ROW SHARE lock on the InternalId record + def track_greatest(new_value) + subject.transaction do + (lookup || create_record).track_greatest_and_save!(new_value) + end + end + private # Retrieve InternalId record for (project, usage) combination, if it exists diff --git a/app/models/issue.rb b/app/models/issue.rb index bf3cc63968d..0d135f54038 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -14,6 +14,7 @@ class Issue < ActiveRecord::Base include TimeTrackable include ThrottledTouch include IgnorableColumn + include LabelEventable ignore_column :assignee_id, :branch_name, :deleted_at diff --git a/app/models/label.rb b/app/models/label.rb index f5c60177d52..96c1515b41a 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -4,6 +4,7 @@ class Label < ActiveRecord::Base include CacheMarkdownField include Referable include Subscribable + include Gitlab::SQL::Pattern # Represents a "No Label" state used for filtering Issues and Merge # Requests that have no label assigned. @@ -105,6 +106,17 @@ class Label < ActiveRecord::Base nil end + # Searches for labels with a matching title or description. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. + def self.search(query) + fuzzy_search(query, [:title, :description]) + end + def open_issues_count(user = nil) issues_count(user, state: 'opened') end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index cf4ee17d2b0..06642c15585 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -12,6 +12,7 @@ class MergeRequest < ActiveRecord::Base include EachBatch include ThrottledTouch include Gitlab::Utils::StrongMemoize + include LabelEventable ignore_column :locked_at, :ref_fetched, diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b18a8623ce4..d9393b4e545 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -251,15 +251,13 @@ class MergeRequestDiff < ActiveRecord::Base end def load_diffs(options) - raw = merge_request_diff_files.map(&:to_hash) + collection = merge_request_diff_files if paths = options[:paths] - raw = raw.select do |diff| - paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) - end + collection = collection.where('old_path IN (?) OR new_path IN (?)', paths, paths) end - Gitlab::Git::DiffCollection.new(raw, options) + Gitlab::Git::DiffCollection.new(collection.map(&:to_hash), options) end def load_commits diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 1f3b3fda1eb..f2b2d291da9 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -151,6 +151,10 @@ class Milestone < ActiveRecord::Base reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) when 'due_date_desc' reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC')) + when 'name_asc' + reorder(Arel::Nodes::Ascending.new(arel_table[:title].lower)) + when 'name_desc' + reorder(Arel::Nodes::Descending.new(arel_table[:title].lower)) when 'start_date_asc' reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC')) when 'start_date_desc' diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 75341cab59b..b974309aeb6 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -124,6 +124,7 @@ class Namespace < ActiveRecord::Base def to_param full_path end + alias_method :flipper_id, :to_param def human_name owner_name @@ -306,7 +307,6 @@ class Namespace < ActiveRecord::Base def write_projects_repository_config all_projects.find_each do |project| - project.expires_full_path_cache # we need to clear cache to validate renames correctly project.write_repository_config end end diff --git a/app/models/note.rb b/app/models/note.rb index 67a507a5685..969d34ae09a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -103,7 +103,7 @@ class Note < ActiveRecord::Base scope :inc_author_project, -> { includes(:project, :author) } scope :inc_author, -> { includes(:author) } scope :inc_relations_for_view, -> do - includes(:project, :author, :updated_by, :resolved_by, :award_emoji, + includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji, :system_note_metadata, :note_diff_file) end diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb new file mode 100644 index 00000000000..400d6c407a7 --- /dev/null +++ b/app/models/programming_language.rb @@ -0,0 +1,4 @@ +class ProgrammingLanguage < ActiveRecord::Base + validates :name, presence: true + validates :color, allow_blank: false, color: true +end diff --git a/app/models/project.rb b/app/models/project.rb index cb10b06c233..16d63639141 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -194,6 +194,7 @@ class Project < ActiveRecord::Base has_many :hooks, class_name: 'ProjectHook' has_many :protected_branches has_many :protected_tags + has_many :repository_languages, -> { order "share DESC" } has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' @@ -550,6 +551,10 @@ class Project < ActiveRecord::Base repository.commit_by(oid: oid) end + def commits_by(oids:) + repository.commits_by(oids: oids) + end + # ref can't be HEAD, can only be branch/tag name or SHA def latest_successful_builds_for(ref = default_branch) latest_pipeline = pipelines.latest_successful_for(ref) @@ -1240,8 +1245,6 @@ class Project < ActiveRecord::Base return true if skip_disk_validation return false unless repository_storage - expires_full_path_cache # we need to clear cache to validate renames correctly - # Check if repository with same path already exists on disk we can # skip this for the hashed storage because the path does not change if legacy_storage? && repository_with_same_path_already_exists? @@ -1620,7 +1623,6 @@ class Project < ActiveRecord::Base # When we import a project overwriting the original project, there # is a move operation. In that case we don't want to send the instructions. send_move_instructions(full_path_was) unless import_started? - expires_full_path_cache self.old_path_with_namespace = full_path_was SystemHooksService.new.execute_hooks_for(self, :rename) diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 206d6ac5e88..781a197d56f 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -7,7 +7,7 @@ class ProjectStatistics < ActiveRecord::Base before_save :update_storage_size COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze - INCREMENTABLE_COLUMNS = [:build_artifacts_size].freeze + INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size] }.freeze def total_repository_size repository_size + lfs_objects_size @@ -40,11 +40,28 @@ class ProjectStatistics < ActiveRecord::Base self.storage_size = repository_size + lfs_objects_size + build_artifacts_size end + # Since this incremental update method does not call update_storage_size above, + # we have to update the storage_size here as additional column. + # Additional columns are updated depending on key => [columns], which allows + # to update statistics which are and also those which aren't included in storage_size + # or any other additional summary column in the future. def self.increment_statistic(project_id, key, amount) - raise ArgumentError, "Cannot increment attribute: #{key}" unless key.in?(INCREMENTABLE_COLUMNS) + raise ArgumentError, "Cannot increment attribute: #{key}" unless INCREMENTABLE_COLUMNS.key?(key) return if amount == 0 where(project_id: project_id) - .update_all(["#{key} = COALESCE(#{key}, 0) + (?)", amount]) + .columns_to_increment(key, amount) + end + + def self.columns_to_increment(key, amount) + updates = ["#{key} = COALESCE(#{key}, 0) + (#{amount})"] + + if (additional = INCREMENTABLE_COLUMNS[key]) + additional.each do |column| + updates << "#{column} = COALESCE(#{column}, 0) + (#{amount})" + end + end + + update_all(updates.join(', ')) end end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 2cc5e521e3d..833faf3bc82 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -50,13 +50,13 @@ class RemoteMirror < ActiveRecord::Base state :failed after_transition any => :started do |remote_mirror, _| - Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path) + Gitlab::Metrics.add_event(:remote_mirrors_running) remote_mirror.update(last_update_started_at: Time.now) end after_transition started: :finished do |remote_mirror, _| - Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path) + Gitlab::Metrics.add_event(:remote_mirrors_finished) timestamp = Time.now remote_mirror.update!( @@ -65,7 +65,7 @@ class RemoteMirror < ActiveRecord::Base end after_transition started: :failed do |remote_mirror, _| - Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path) + Gitlab::Metrics.add_event(:remote_mirrors_failed) remote_mirror.update(last_update_at: Time.now) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 192865dfd61..69f375dc6f3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -237,6 +237,12 @@ class Repository false end + def languages + return [] if empty? + + raw_repository.languages(root_ref) + end + # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -314,6 +320,8 @@ class Repository # types - An Array of file types (e.g. `:readme`) used to refresh extra # caches. def refresh_method_caches(types) + return if types.empty? + to_refresh = [] types.each do |type| @@ -432,6 +440,8 @@ class Repository # Runs code after a repository has been forked/imported. def after_import expire_content_cache + + DetectRepositoryLanguagesWorker.perform_async(project.id, project.owner.id) end # Runs code after a new commit has been pushed. @@ -1031,7 +1041,7 @@ class Repository end def repository_event(event, tags = {}) - Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags)) + Gitlab::Metrics.add_event(event, tags) end def initialize_raw_repository diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb new file mode 100644 index 00000000000..f467d4eafa3 --- /dev/null +++ b/app/models/repository_language.rb @@ -0,0 +1,12 @@ +class RepositoryLanguage < ActiveRecord::Base + belongs_to :project + belongs_to :programming_language + + default_scope { includes(:programming_language) } + + validates :project, presence: true + validates :share, inclusion: { in: 0..100, message: "The share of a lanuage is between 0 and 100" } + validates :programming_language, uniqueness: { scope: :project_id } + + delegate :name, :color, to: :programming_language +end diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb new file mode 100644 index 00000000000..42c255fcd1e --- /dev/null +++ b/app/models/resource_label_event.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# This model is not used yet, it will be used for: +# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 +class ResourceLabelEvent < ActiveRecord::Base + belongs_to :user + belongs_to :issue + belongs_to :merge_request + belongs_to :label + + validates :user, presence: true, on: :create + validates :label, presence: true, on: :create + validate :exactly_one_issuable + + enum action: { + add: 1, + remove: 2 + } + + def self.issuable_columns + %i(issue_id merge_request_id).freeze + end + + def issuable + issue || merge_request + end + + private + + def exactly_one_issuable + if self.class.issuable_columns.count { |attr| self[attr] } != 1 + errors.add(:base, "Exactly one of #{self.class.issuable_columns.join(', ')} is required") + end + end +end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index f82d3a5d00f..5b394e3fa79 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -51,6 +51,7 @@ class Snippet < ActiveRecord::Base scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } + scope :inc_relations_for_view, -> { includes(author: :status) } participant :author participant :notes_with_associations diff --git a/app/models/user.rb b/app/models/user.rb index 0de8a6d057f..fdf3618b4dc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -143,6 +143,8 @@ class User < ActiveRecord::Base has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' + has_one :status, class_name: 'UserStatus' + # # Validations # @@ -250,6 +252,7 @@ class User < ActiveRecord::Base scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } 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 :confirmed, -> { where.not(confirmed_at: nil) } def self.with_two_factor_indistinct joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") @@ -295,14 +298,17 @@ class User < ActiveRecord::Base end # Find a User by their primary email or any associated secondary email - def find_by_any_email(email) - by_any_email(email).take + def find_by_any_email(email, confirmed: false) + by_any_email(email, confirmed: confirmed).take end # Returns a relation containing all the users for the given Email address - def by_any_email(email) + def by_any_email(email, confirmed: false) users = where(email: email) + users = users.confirmed if confirmed + emails = joins(:emails).where(emails: { email: email }) + emails = emails.confirmed if confirmed union = Gitlab::SQL::Union.new([users, emails]) from("(#{union.to_sql}) #{table_name}") diff --git a/app/models/user_status.rb b/app/models/user_status.rb new file mode 100644 index 00000000000..2bbb0c59ac1 --- /dev/null +++ b/app/models/user_status.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class UserStatus < ActiveRecord::Base + include CacheMarkdownField + + self.primary_key = :user_id + + DEFAULT_EMOJI = 'speech_balloon'.freeze + + belongs_to :user + + validates :user, presence: true + validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names } + validates :message, length: { maximum: 100 }, allow_blank: true + + cache_markdown_field :message, pipeline: :emoji +end |