diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2018-02-07 17:40:46 -0600 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2018-02-07 17:40:46 -0600 |
commit | c1c74e608625eab143f44ef6940e64cca6af0a0c (patch) | |
tree | c5ca4a8a39946f54878a3ecde6b4adcda11d2d9b /app/models | |
parent | cecea7529fb37893af6bf514a53e239a3be1eea6 (diff) | |
parent | 8900b23eab6abd5a6c01278fa0da18d5bed98491 (diff) | |
download | gitlab-ce-c1c74e608625eab143f44ef6940e64cca6af0a0c.tar.gz |
Merge branch 'master' into fix-no-template
Diffstat (limited to 'app/models')
72 files changed, 819 insertions, 482 deletions
diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 76cfe28742a..dcd14c08f3c 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -11,6 +11,7 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader + has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent CACHE_KEY = 'current_appearance'.freeze diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 253e213af81..0dee6df525d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -117,6 +117,11 @@ class ApplicationSetting < ActiveRecord::Base validates :repository_storages, presence: true validate :check_repository_storages + validates :auto_devops_domain, + allow_blank: true, + hostname: { allow_numeric_hostname: true, require_valid_tld: true }, + if: :auto_devops_enabled? + validates :enabled_git_access_protocol, inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true } @@ -261,6 +266,7 @@ class ApplicationSetting < ActiveRecord::Base { after_sign_up_text: nil, akismet_enabled: 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'], @@ -417,6 +423,7 @@ class ApplicationSetting < ActiveRecord::Base super(group_full_path) Gitlab::PerformanceBar.expire_allowed_user_ids_cache end + return end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6012dbba1b9..20534b8eed0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -8,6 +8,7 @@ module Ci MissingDependenciesError = Class.new(StandardError) + belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' @@ -20,6 +21,7 @@ module Ci has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # 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 # The "environment" field for builds is a String, and is the unexpanded name def persisted_environment @@ -291,7 +293,7 @@ module Ci def repo_url auth = "gitlab-ci-token:#{ensure_token!}@" - project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| + project.http_url_to_repo.sub(%r{^https?://}) do |prefix| prefix + auth end end @@ -465,7 +467,7 @@ module Ci if cache && project.jobs_cache_index cache = cache.merge( - key: "#{cache[:key]}:#{project.jobs_cache_index}") + key: "#{cache[:key]}_#{project.jobs_cache_index}") end [cache] diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 84fc6863567..0a599f72bc7 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -9,9 +9,12 @@ module Ci mount_uploader :file, JobArtifactUploader + delegate :open, :exists?, to: :file + enum file_type: { archive: 1, - metadata: 2 + metadata: 2, + trace: 3 } def self.artifacts_size_for(project) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d4690da3be6..2abe90dd181 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -7,7 +7,7 @@ module Ci include Presentable include Gitlab::OptimisticLocking - belongs_to :project + belongs_to :project, inverse_of: :pipelines belongs_to :user belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' @@ -394,7 +394,7 @@ module Ci @config_processor ||= begin Gitlab::Ci::YamlProcessor.new(ci_yaml_file) - rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e + rescue Gitlab::Ci::YamlProcessor::ValidationError => e self.yaml_errors = e.message nil rescue @@ -524,7 +524,7 @@ module Ci return unless sha project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) - rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal + rescue GRPC::NotFound, GRPC::Internal nil end diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 10ead6b6d3b..b6abc3d7681 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -2,8 +2,9 @@ module Ci class PipelineSchedule < ActiveRecord::Base extend Gitlab::Ci::Model include Importable + include IgnorableColumn - acts_as_paranoid + ignore_column :deleted_at belongs_to :project belongs_to :owner, class_name: 'User' diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index dcbb397fb78..13c784bea0d 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,9 +2,11 @@ module Ci class Runner < ActiveRecord::Base extend Gitlab::Ci::Model include Gitlab::SQL::Pattern + include RedisCacheable RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour + UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes AVAILABLE_SCOPES = %w[specific shared active paused online].freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze @@ -47,6 +49,8 @@ module Ci ref_protected: 1 } + cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at + # Searches for runners matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -152,6 +156,18 @@ module Ci ensure_runner_queue_value == value if value.present? end + def update_cached_info(values) + values = values&.slice(:version, :revision, :platform, :architecture) || {} + values[:contacted_at] = Time.now + + cache_attributes(values) + + if persist_cached_data? + self.assign_attributes(values) + self.save if self.changed? + end + end + private def cleanup_runner_queue @@ -164,6 +180,17 @@ module Ci "runner:build_queue:#{self.token}" end + def persist_cached_data? + # Use a random threshold to prevent beating DB updates. + # It generates a distribution between [40m, 80m]. + + contacted_at_max_age = UPDATE_DB_RUNNER_INFO_EVERY + Random.rand(UPDATE_DB_RUNNER_INFO_EVERY) + + real_contacted_at = read_attribute(:contacted_at) + real_contacted_at.nil? || + (Time.now - real_contacted_at) >= contacted_at_max_age + end + def tag_constraints unless has_tags? || run_untagged? errors.add(:tags_list, diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index b5290bcaf53..aa065e33739 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,8 +1,9 @@ module Ci class Trigger < ActiveRecord::Base extend Gitlab::Ci::Model + include IgnorableColumn - acts_as_paranoid + ignore_column :deleted_at belongs_to :project belongs_to :owner, class_name: "User" diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 9024f1df1cd..aa5cf97756f 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -17,8 +17,12 @@ module Clusters 'stable/nginx-ingress' end + def chart_values_file + "#{Rails.root}/vendor/#{name}/values.yaml" + end + def install_command - Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart) + Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) end end end diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 9b0787ee6ca..aa22e9d5d58 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -10,10 +10,26 @@ module Clusters default_value_for :version, VERSION + state_machine :status do + after_transition any => [:installed] do |application| + application.cluster.projects.each do |project| + project.find_or_initialize_service('prometheus').update(active: true) + end + end + end + def chart 'stable/prometheus' end + def service_name + 'prometheus-prometheus-server' + end + + def service_port + 80 + end + def chart_values_file "#{Rails.root}/vendor/#{name}/values.yaml" end @@ -21,6 +37,22 @@ module Clusters def install_command Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) end + + def proxy_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(headers: kube_client.headers) + RestClient::Resource.new(proxy_url, options) + end + + private + + def kube_client + cluster&.kubeclient + end end end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 5ecbd4cbceb..8678f70f78c 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -49,6 +49,9 @@ module Clusters scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } + scope :for_environment, -> (env) { where(environment_scope: ['*', '', env.slug]) } + scope :for_all_environments, -> { where(environment_scope: ['*', '']) } + def status_name if provider provider.status_name diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 9160a169452..7ce8befeeeb 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -1,7 +1,6 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base - include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching @@ -169,7 +168,7 @@ module Clusters { token: token, ca_pem: ca_pem, - max_session_time: current_application_settings.terminal_max_session_time + max_session_time: Gitlab::CurrentSettings.terminal_max_session_time } end @@ -181,7 +180,7 @@ module Clusters return unless managed? if api_url_changed? || token_changed? || ca_pem_changed? - errors.add(:base, "cannot modify managed cluster") + errors.add(:base, _('Cannot modify managed Kubernetes cluster')) return false end diff --git a/app/models/commit.rb b/app/models/commit.rb index 39d7f5b159d..2d2d89af030 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -238,6 +238,10 @@ class Commit notes.includes(:author) end + def merge_requests + @merge_requests ||= project.merge_requests.by_commit_sha(sha) + end + def method_missing(method, *args, &block) @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end @@ -342,10 +346,11 @@ class Commit @merged_merge_request_hash[current_user] end - def has_been_reverted?(current_user, noteable = self) + def has_been_reverted?(current_user, notes_association = nil) ext = all_references(current_user) + notes_association ||= notes_with_associations - noteable.notes_with_associations.system.each do |note| + notes_association.system.each do |note| note.all_references(current_user, extractor: ext) end @@ -367,19 +372,19 @@ class Commit # uri_type('doc/README.md') # => :blob # uri_type('doc/logo.png') # => :raw # uri_type('doc/api') # => :tree - # uri_type('not/found') # => :nil + # uri_type('not/found') # => nil # # Returns a symbol def uri_type(path) - entry = @raw.rugged_tree_entry(path) + entry = @raw.tree_entry(path) + return unless entry + if entry[:type] == :blob blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob.image? || blob.video? ? :raw : :blob else entry[:type] end - rescue Rugged::TreeError - nil end def raw_diffs(*args) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c0263c0b4e2..3469d5d795c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base end def group_name - name.to_s.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip + name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip end def failed_but_allowed? diff --git a/app/models/concerns/artifact_migratable.rb b/app/models/concerns/artifact_migratable.rb index 0460439e9e6..ff52ca64459 100644 --- a/app/models/concerns/artifact_migratable.rb +++ b/app/models/concerns/artifact_migratable.rb @@ -39,7 +39,6 @@ module ArtifactMigratable end def artifacts_size - read_attribute(:artifacts_size).to_i + - job_artifacts_archive&.size.to_i + job_artifacts_metadata&.size.to_i + read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 10659030910..d35e37935fb 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -1,6 +1,30 @@ module Avatarable extend ActiveSupport::Concern + included do + prepend ShadowMethods + + validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + + mount_uploader :avatar, AvatarUploader + end + + module ShadowMethods + def avatar_url(**args) + # We use avatar_path instead of overriding avatar_url because of carrierwave. + # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864 + + avatar_path(only_path: args.fetch(:only_path, true)) || super + end + end + + def avatar_type + unless self.avatar.image? + self.errors.add :avatar, "only images allowed" + end + end + def avatar_path(only_path: true) return unless self[:avatar].present? diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb index db9770fabf4..8b3c55387b3 100644 --- a/app/models/concerns/discussion_on_diff.rb +++ b/app/models/concerns/discussion_on_diff.rb @@ -37,6 +37,8 @@ module DiscussionOnDiff # Returns an array of at most 16 highlighted lines above a diff note def truncated_diff_lines(highlight: true) + return [] if diff_line.nil? && first_note.is_a?(LegacyDiffNote) + lines = highlight ? highlighted_diff_lines : diff_lines initial_line_index = [diff_line.index - NUMBER_OF_TRUNCATED_DIFF_LINES + 1, 0].max diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index a3d0ac8d862..01079fb8bd6 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -10,7 +10,6 @@ module InternalId if iid.blank? parent = project || group records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend - records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) self.iid = max_iid.to_i + 1 diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 4251561a0a0..7049f340c9d 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -314,6 +314,7 @@ module Issuable includes = [] includes << :author unless notes.authors_loaded? includes << :award_emoji unless notes.award_emojis_loaded? + if includes.any? notes.includes(includes) else diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb index dcb3b2b5ff3..935e9d10133 100644 --- a/app/models/concerns/loaded_in_group_list.rb +++ b/app/models/concerns/loaded_in_group_list.rb @@ -25,6 +25,7 @@ module LoadedInGroupList base_count = projects.project(Arel.star.count.as('preloaded_project_count')) .where(projects[:namespace_id].eq(namespaces[:id])) + if archived == 'only' base_count.where(projects[:archived].eq(true)) elsif Gitlab::Utils.to_boolean(archived) diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb new file mode 100644 index 00000000000..b889f4202dc --- /dev/null +++ b/app/models/concerns/redis_cacheable.rb @@ -0,0 +1,41 @@ +module RedisCacheable + extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize + + CACHED_ATTRIBUTES_EXPIRY_TIME = 24.hours + + class_methods do + def cached_attr_reader(*attributes) + attributes.each do |attribute| + define_method("#{attribute}") do + cached_attribute(attribute) || read_attribute(attribute) + end + end + end + end + + def cached_attribute(attribute) + (cached_attributes || {})[attribute] + end + + def cache_attributes(values) + Gitlab::Redis::SharedState.with do |redis| + redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME) + end + end + + private + + def cache_attribute_key + "cache:#{self.class.name}:#{self.id}:attributes" + end + + def cached_attributes + strong_memoize(:cached_attributes) do + Gitlab::Redis::SharedState.with do |redis| + data = redis.get(cache_attribute_key) + JSON.parse(data, symbolize_names: true) if data + end + end + end +end diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index b6c7b6735b9..7c236369793 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -1,5 +1,6 @@ module ResolvableDiscussion extend ActiveSupport::Concern + include ::Gitlab::Utils::StrongMemoize included do # A number of properties of this `Discussion`, like `first_note` and `resolvable?`, are memoized. @@ -31,27 +32,37 @@ module ResolvableDiscussion end def resolvable? - @resolvable ||= potentially_resolvable? && notes.any?(&:resolvable?) + strong_memoize(:resolvable) do + potentially_resolvable? && notes.any?(&:resolvable?) + end end def resolved? - @resolved ||= resolvable? && notes.none?(&:to_be_resolved?) + strong_memoize(:resolved) do + resolvable? && notes.none?(&:to_be_resolved?) + end end def first_note - @first_note ||= notes.first + strong_memoize(:first_note) do + notes.first + end end def first_note_to_resolve return unless resolvable? - @first_note_to_resolve ||= notes.find(&:to_be_resolved?) # rubocop:disable Gitlab/ModuleWithInstanceVariables + strong_memoize(:first_note_to_resolve) do + notes.find(&:to_be_resolved?) + end end def last_resolved_note return unless resolved? - @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last # rubocop:disable Gitlab/ModuleWithInstanceVariables + strong_memoize(:last_resolved_note) do + resolved_notes.sort_by(&:resolved_at).last + end end def resolved_notes @@ -93,8 +104,8 @@ module ResolvableDiscussion # Set the notes array to the updated notes @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables - self.class.memoized_values.each do |var| - instance_variable_set(:"@#{var}", nil) + self.class.memoized_values.each do |name| + clear_memoization(name) end end end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 5c1cce98ad4..dfd7d94450b 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -7,11 +7,12 @@ module Routable has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - validates_associated :route validates :route, presence: true scope :with_route, -> { includes(:route) } + after_validation :set_path_errors + before_validation do if full_path_changed? || full_name_changed? prepare_route @@ -125,6 +126,11 @@ module Routable private + def set_path_errors + route_path_errors = self.errors.delete(:"route.path") + 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 diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb index 67ecf470f7e..703a72c355c 100644 --- a/app/models/concerns/sha_attribute.rb +++ b/app/models/concerns/sha_attribute.rb @@ -3,6 +3,7 @@ module ShaAttribute module ClassMethods def sha_attribute(name) + return if ENV['STATIC_VERIFICATION'] return unless table_exists? column = columns.find { |c| c.name == name.to_s } diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 99dbd4fbacf..67a988addbe 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -14,7 +14,11 @@ module Storage # Ensure old directory exists before moving it gitlab_shell.add_namespace(repository_storage_path, full_path_was) + # Ensure new directory exists before moving it (if there's a parent) + gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent + unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" # if we cannot move namespace directory we should rollback @@ -87,20 +91,10 @@ module Storage remove_exports! end - def remove_exports! - Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) - end - - def export_path - File.join(Gitlab::ImportExport.storage_path, full_path_was) - end + def remove_legacy_exports! + legacy_export_path = File.join(Gitlab::ImportExport.storage_path, full_path_was) - def full_path_was - if parent - parent.full_path + '/' + path_was - else - path_was - end + FileUtils.rm_rf(legacy_export_path) end end end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index d07041c2fdf..ccd6f0e0a7d 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -9,13 +9,13 @@ require 'task_list/filter' module Taskable COMPLETED = 'completed'.freeze INCOMPLETE = 'incomplete'.freeze - ITEM_PATTERN = / + ITEM_PATTERN = %r{ ^ \s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list \s+ # whitespace prefix has to be always presented for a list item (\[\s\]|\[[xX]\]) # checkbox (\s.+) # followed by whitespace and some text. - /x + }x def self.get_tasks(content) content.to_s.scan(ITEM_PATTERN).map do |checkbox, label| diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb new file mode 100644 index 00000000000..ec0ed3b795a --- /dev/null +++ b/app/models/concerns/triggerable_hooks.rb @@ -0,0 +1,40 @@ +module TriggerableHooks + AVAILABLE_TRIGGERS = { + repository_update_hooks: :repository_update_events, + push_hooks: :push_events, + tag_push_hooks: :tag_push_events, + issue_hooks: :issues_events, + confidential_issue_hooks: :confidential_issues_events, + note_hooks: :note_events, + merge_request_hooks: :merge_requests_events, + job_hooks: :job_events, + pipeline_hooks: :pipeline_events, + wiki_page_hooks: :wiki_page_events + }.freeze + + extend ActiveSupport::Concern + + class_methods do + attr_reader :triggerable_hooks + + attr_reader :triggers + + def hooks_for(trigger) + callable_scopes = triggers.keys + [:all] + return none unless callable_scopes.include?(trigger) + + public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend + end + + private + + def triggerable_hooks(hooks) + triggers = AVAILABLE_TRIGGERS.slice(*hooks) + @triggers = triggers + + triggers.each do |trigger, event| + scope trigger, -> { where(event => true) } + end + end + end +end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index eae5eee4fee..c2e0a5fa126 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -1,10 +1,16 @@ class DeployKey < Key - has_many :deploy_keys_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + include IgnorableColumn + + has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :deploy_keys_projects scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) } scope :are_public, -> { where(public: true) } + ignore_column :can_push + + accepts_nested_attributes_for :deploy_keys_projects + def private? !public? end @@ -22,10 +28,18 @@ class DeployKey < Key end def has_access_to?(project) - projects.include?(project) + deploy_keys_project_for(project).present? end def can_push_to?(project) - can_push? && has_access_to?(project) + !!deploy_keys_project_for(project)&.can_push? + end + + def deploy_keys_project_for(project) + deploy_keys_projects.find_by(project: project) + end + + def projects_with_write_access + Project.preload(:route).where(id: deploy_keys_projects.with_write_access.select(:project_id)) end end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index b37b9bfbdac..6eef12c4373 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -1,8 +1,14 @@ class DeployKeysProject < ActiveRecord::Base belongs_to :project - belongs_to :deploy_key + belongs_to :deploy_key, inverse_of: :deploy_keys_projects - validates :deploy_key_id, presence: true + scope :without_project_deleted, -> { joins(:project).where(projects: { pending_delete: false }) } + scope :in_project, ->(project) { where(project: project) } + scope :with_write_access, -> { where(can_push: true) } + + accepts_nested_attributes_for :deploy_key + + validates :deploy_key, presence: true validates :deploy_key_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 7bcded5b5e1..3aed071dd49 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -45,14 +45,7 @@ class Deployment < ActiveRecord::Base def includes_commit?(commit) return false unless commit - # Before 8.10, deployments didn't have keep-around refs. Any deployment - # created before then could have a `sha` referring to a commit that no - # longer exists in the repository, so just ignore those. - begin - project.repository.ancestor?(commit.id, sha) - rescue Rugged::OdbError - false - end + project.repository.ancestor?(commit.id, sha) end def update_merge_request_metrics! diff --git a/app/models/environment.rb b/app/models/environment.rb index bf69b4c50f0..2f6eae605ee 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -115,7 +115,7 @@ class Environment < ActiveRecord::Base def formatted_external_url return nil unless external_url - external_url.gsub(/\A.*?:\/\//, '') + external_url.gsub(%r{\A.*?://}, '') end def stop_action? diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index c0864769314..dc2f6817190 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -44,10 +44,10 @@ class GlobalMilestone def self.group_milestones_states_count(group) return STATE_COUNT_HASH unless group - params = { group_ids: [group.id], state: 'all', order: nil } + params = { group_ids: [group.id], state: 'all' } relation = MilestonesFinder.new(params).execute - grouped_by_state = relation.group(:state).count + grouped_by_state = relation.reorder(nil).group(:state).count { opened: grouped_by_state['active'] || 0, @@ -60,10 +60,10 @@ class GlobalMilestone def self.legacy_group_milestone_states_count(projects) return STATE_COUNT_HASH unless projects - params = { project_ids: projects.map(&:id), state: 'all', order: nil } + params = { project_ids: projects.map(&:id), state: 'all' } relation = MilestonesFinder.new(params).execute - project_milestones_by_state_and_title = relation.group(:state, :title).count + project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count opened = count_by_state(project_milestones_by_state_and_title, 'active') closed = count_by_state(project_milestones_by_state_and_title, 'closed') diff --git a/app/models/group.rb b/app/models/group.rb index fddace03387..75bf013ecd2 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -29,18 +29,17 @@ class Group < Namespace has_many :variables, class_name: 'Ci::GroupVariable' has_many :custom_attributes, class_name: 'GroupCustomAttribute' - validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } + has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + + accepts_nested_attributes_for :variables, allow_destroy: true + validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_sub_groups validate :visibility_level_allowed_by_parent - - validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + validates :variables, variable_duplicates: true validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } - mount_uploader :avatar, AvatarUploader - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - after_create :post_create_hook after_destroy :post_destroy_hook after_save :update_two_factor_requirement @@ -116,12 +115,6 @@ class Group < Namespace visibility_level_allowed_by_sub_groups?(level) end - def avatar_url(**args) - # We use avatar_path instead of overriding avatar_url because of carrierwave. - # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864 - avatar_path(args) - end - def lfs_enabled? return false unless Gitlab.config.lfs.enabled return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil? @@ -284,12 +277,6 @@ class Group < Namespace list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten end - def full_path_was - return path_was unless has_parent? - - "#{parent.full_path}/#{path_was}" - end - def group_member(user) if group_members.loaded? group_members.find { |gm| gm.user_id == user.id } diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index a8c424a6614..b6dd39b860b 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -1,19 +1,17 @@ class ProjectHook < WebHook - TRIGGERS = { - push_hooks: :push_events, - tag_push_hooks: :tag_push_events, - issue_hooks: :issues_events, - confidential_issue_hooks: :confidential_issues_events, - note_hooks: :note_events, - merge_request_hooks: :merge_requests_events, - job_hooks: :job_events, - pipeline_hooks: :pipeline_events, - wiki_page_hooks: :wiki_page_events - }.freeze + include TriggerableHooks - TRIGGERS.each do |trigger, event| - scope trigger, -> { where(event => true) } - end + triggerable_hooks [ + :push_hooks, + :tag_push_hooks, + :issue_hooks, + :confidential_issue_hooks, + :note_hooks, + :merge_request_hooks, + :job_hooks, + :pipeline_hooks, + :wiki_page_hooks + ] belongs_to :project validates :project, presence: true diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 180c479c41b..0528266e5b3 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -1,14 +1,14 @@ class SystemHook < WebHook - TRIGGERS = { - repository_update_hooks: :repository_update_events, - push_hooks: :push_events, - tag_push_hooks: :tag_push_events - }.freeze + include TriggerableHooks - TRIGGERS.each do |trigger, event| - scope trigger, -> { where(event => true) } - end + triggerable_hooks [ + :repository_update_hooks, + :push_hooks, + :tag_push_hooks, + :merge_request_hooks + ] default_value_for :push_events, false default_value_for :repository_update_events, true + default_value_for :merge_requests_events, false end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 5a70e114f56..27729deeac9 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :url, presence: true, url: true + validates :token, format: { without: /\n/ } def execute(data, hook_name) WebHookService.new(self, data, hook_name).execute diff --git a/app/models/issue.rb b/app/models/issue.rb index ad4a3c737ff..93628b456f2 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -12,7 +12,7 @@ class Issue < ActiveRecord::Base include ThrottledTouch include IgnorableColumn - ignore_column :assignee_id, :branch_name + ignore_column :assignee_id, :branch_name, :deleted_at DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -78,8 +78,6 @@ class Issue < ActiveRecord::Base end end - acts_as_paranoid - class << self alias_method :in_parents, :in_projects end diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb index 06d760b6a89..326b9eb7ad5 100644 --- a/app/models/issue_assignee.rb +++ b/app/models/issue_assignee.rb @@ -1,6 +1,4 @@ class IssueAssignee < ActiveRecord::Base - extend Gitlab::CurrentSettings - belongs_to :issue belongs_to :assignee, class_name: "User", foreign_key: :user_id end diff --git a/app/models/key.rb b/app/models/key.rb index a3f8a5d6dc7..ae5769c0627 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -1,7 +1,6 @@ require 'digest/md5' class Key < ActiveRecord::Base - include Gitlab::CurrentSettings include AfterCommitQueue include Sortable @@ -34,9 +33,8 @@ class Key < ActiveRecord::Base after_destroy :refresh_user_cache def key=(value) - value&.delete!("\n\r") - value.strip! unless value.blank? - write_attribute(:key, value) + write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) + @public_key = nil end @@ -98,13 +96,13 @@ class Key < ActiveRecord::Base def generate_fingerprint self.fingerprint = nil - return unless self.key.present? + return unless public_key.valid? self.fingerprint = public_key.fingerprint end def key_meets_restrictions - restriction = current_application_settings.key_restriction_for(public_key.type) + restriction = Gitlab::CurrentSettings.key_restriction_for(public_key.type) if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE errors.add(:key, forbidden_key_type_message) @@ -115,7 +113,7 @@ class Key < ActiveRecord::Base def forbidden_key_type_message allowed_types = - current_application_settings + Gitlab::CurrentSettings .allowed_key_types .map(&:upcase) .to_sentence(last_word_connector: ', or ', two_words_connector: ' or ') diff --git a/app/models/label.rb b/app/models/label.rb index b5bfa6ea2dd..7538f2d8718 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -132,6 +132,7 @@ class Label < ActiveRecord::Base else priorities.find_by(project: project) end + priority.try(:priority) end diff --git a/app/models/lfs_file_lock.rb b/app/models/lfs_file_lock.rb new file mode 100644 index 00000000000..50bb6ca382d --- /dev/null +++ b/app/models/lfs_file_lock.rb @@ -0,0 +1,12 @@ +class LfsFileLock < ActiveRecord::Base + belongs_to :project + belongs_to :user + + validates :project_id, :user_id, :path, presence: true + + def can_be_unlocked_by?(current_user, forced = false) + return true if current_user.id == user_id + + forced && current_user.can?(:admin_project, project) + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ef58816937c..d025062f562 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -11,7 +11,8 @@ class MergeRequest < ActiveRecord::Base include Gitlab::Utils::StrongMemoize ignore_column :locked_at, - :ref_fetched + :ref_fetched, + :deleted_at belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" @@ -139,7 +140,9 @@ class MergeRequest < ActiveRecord::Base scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :from_source_branches, ->(branches) { where(source_branch: branches) } - + scope :by_commit_sha, ->(sha) do + where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil) + end scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } scope :assigned, -> { where("assignee_id IS NOT NULL") } @@ -150,8 +153,6 @@ class MergeRequest < ActiveRecord::Base after_save :keep_around_commit - acts_as_paranoid - def self.reference_prefix '!' end @@ -617,12 +618,12 @@ class MergeRequest < ActiveRecord::Base can_be_merged? && !should_be_rebased? end - def mergeable_state?(skip_ci_check: false) + def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) return false unless open? return false if work_in_progress? return false if broken? return false unless skip_ci_check || mergeable_ci_state? - return false unless mergeable_discussions_state? + return false unless skip_discussions_check || mergeable_discussions_state? true end @@ -794,6 +795,7 @@ class MergeRequest < ActiveRecord::Base if !include_description && closes_issues_references.present? message << "Closes #{closes_issues_references.to_sentence}" end + message << "#{description}" if include_description && description.present? message << "See merge request #{to_reference(full: true)}" @@ -982,7 +984,22 @@ class MergeRequest < ActiveRecord::Base end def can_be_reverted?(current_user) - merge_commit && !merge_commit.has_been_reverted?(current_user, self) + return false unless merge_commit + + merged_at = metrics&.merged_at + notes_association = notes_with_associations + + if merged_at + # It is not guaranteed that Note#created_at will be strictly later than + # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this + # comparison, as will a HA environment if clocks are not *precisely* + # synchronized. Add a minute's leeway to compensate for both possibilities + cutoff = merged_at - 1.minute + + notes_association = notes_association.where('created_at >= ?', cutoff) + end + + !merge_commit.has_been_reverted?(current_user, notes_association) end def can_be_cherry_picked? diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index e35de9b97ee..c1c27ccf3e5 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -28,6 +28,9 @@ class MergeRequestDiff < ActiveRecord::Base end scope :viewable, -> { without_state(:empty) } + scope :by_commit_sha, ->(sha) do + joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil) + end scope :recent, -> { order(id: :desc).limit(100) } @@ -49,6 +52,7 @@ class MergeRequestDiff < ActiveRecord::Base ensure_commit_shas save_commits save_diffs + save keep_around_commits end @@ -56,7 +60,6 @@ class MergeRequestDiff < ActiveRecord::Base self.start_commit_sha ||= merge_request.target_branch_sha self.head_commit_sha ||= merge_request.source_branch_sha self.base_commit_sha ||= find_base_sha - save end # Override head_commit_sha to keep compatibility with merge request diff @@ -195,7 +198,7 @@ class MergeRequestDiff < ActiveRecord::Base end def commits_count - merge_request_diff_commits.size + super || merge_request_diff_commits.size end private @@ -264,13 +267,16 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:state] = :overflow if diff_collection.overflow? end - update(new_attributes) + assign_attributes(new_attributes) end def save_commits MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse) - merge_request_diff_commits.reload + # merge_request_diff_commits.reload is preferred way to reload associated + # objects but it returns cached result for some reason in this case + commits = merge_request_diff_commits(true) + self.commits_count = commits.size end def repository @@ -284,7 +290,7 @@ class MergeRequestDiff < ActiveRecord::Base end def keep_around_commits - [repository, merge_request.source_project.repository].each do |repo| + [repository, merge_request.source_project.repository].uniq.each do |repo| repo.keep_around(start_commit_sha) repo.keep_around(head_commit_sha) repo.keep_around(base_commit_sha) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bdcc9159d26..d95489ee9f2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,15 +1,15 @@ class Namespace < ActiveRecord::Base - acts_as_paranoid without_default_scope: true - include CacheMarkdownField include Sortable include Gitlab::ShellAdapter - include Gitlab::CurrentSettings include Gitlab::VisibilityLevel include Routable include AfterCommitQueue include Storage::LegacyNamespace include Gitlab::SQL::Pattern + include IgnorableColumn + + ignore_column :deleted_at # Prevent users from creating unreasonably deep level of nesting. # The number 20 was taken based on maximum nesting level of @@ -20,6 +20,9 @@ class Namespace < ActiveRecord::Base has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_statistics + + # This should _not_ be `inverse_of: :namespace`, because that would also set + # `user.namespace` when this user creates a group with themselves as `owner`. belongs_to :owner, class_name: "User" belongs_to :parent, class_name: "Namespace" @@ -29,7 +32,6 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, presence: true, - uniqueness: { scope: :parent_id }, length: { maximum: 255 }, namespace_name: true @@ -40,7 +42,6 @@ class Namespace < ActiveRecord::Base namespace_path: true validate :nesting_level_allowed - validate :allowed_path_by_redirects delegate :name, to: :owner, allow_nil: true, prefix: true @@ -52,7 +53,7 @@ class Namespace < ActiveRecord::Base # Legacy Storage specific hooks - after_update :move_dir, if: :path_changed? + after_update :move_dir, if: :path_or_parent_changed? before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir @@ -221,14 +222,33 @@ class Namespace < ActiveRecord::Base has_parent? end - def soft_delete_without_removing_associations - # We can't use paranoia's `#destroy` since this will hard-delete projects. - # Project uses `pending_delete` instead of the acts_as_paranoia gem. - self.deleted_at = Time.now + def full_path_was + if parent_id_was.nil? + path_was + else + previous_parent = Group.find_by(id: parent_id_was) + previous_parent.full_path + '/' + path_was + end + end + + # Exports belonging to projects with legacy storage are placed in a common + # subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove + # them. + # + # Exports of projects using hashed storage are placed in a location defined + # only by the project ID, so each must be removed individually. + def remove_exports! + remove_legacy_exports! + + all_projects.with_storage_feature(:repository).find_each(&:remove_exports) end private + def path_or_parent_changed? + path_changed? || parent_changed? + end + def refresh_access_of_projects_invited_groups Group .joins(project_group_links: :project) @@ -259,16 +279,6 @@ class Namespace < ActiveRecord::Base .update_all(share_with_group_lock: true) end - def allowed_path_by_redirects - return if path.nil? - - errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path? - end - - def namespace_previously_created_with_same_path? - RedirectRoute.permanent.exists?(path: path) - end - 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 diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index aec7b01e23a..c351d2012c6 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -224,6 +224,7 @@ module Network space_base = parents.first.space end end + space_base end diff --git a/app/models/note.rb b/app/models/note.rb index 184fbd5f5ae..cac60845a49 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -3,7 +3,6 @@ # A note of this type is never resolvable. class Note < ActiveRecord::Base extend ActiveModel::Naming - include Gitlab::CurrentSettings include Participable include Mentionable include Awardable @@ -61,7 +60,7 @@ class Note < ActiveRecord::Base belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' - has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :todos has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :system_note_metadata @@ -88,6 +87,7 @@ class Note < ActiveRecord::Base end end + # @deprecated attachments are handler by the MarkdownUploader mount_uploader :attachment, AttachmentUploader # Scopes @@ -195,7 +195,7 @@ class Note < ActiveRecord::Base end def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i + Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i end def hook_attrs diff --git a/app/models/notification_reason.rb b/app/models/notification_reason.rb new file mode 100644 index 00000000000..c3965565022 --- /dev/null +++ b/app/models/notification_reason.rb @@ -0,0 +1,19 @@ +# Holds reasons for a notification to have been sent as well as a priority list to select which reason to use +# above the rest +class NotificationReason + OWN_ACTIVITY = 'own_activity'.freeze + ASSIGNED = 'assigned'.freeze + MENTIONED = 'mentioned'.freeze + + # Priority list for selecting which reason to return in the notification + REASON_PRIORITY = [ + OWN_ACTIVITY, + ASSIGNED, + MENTIONED + ].freeze + + # returns the priority of a reason as an integer + def self.priority(reason) + REASON_PRIORITY.index(reason) || REASON_PRIORITY.length + 1 + end +end diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 183e098d819..472b348a545 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -1,26 +1,19 @@ class NotificationRecipient - attr_reader :user, :type - def initialize( - user, type, - custom_action: nil, - target: nil, - acting_user: nil, - project: nil, - group: nil, - skip_read_ability: false - ) + attr_reader :user, :type, :reason + def initialize(user, type, **opts) unless NotificationSetting.levels.key?(type) || type == :subscription raise ArgumentError, "invalid type: #{type.inspect}" end - @custom_action = custom_action - @acting_user = acting_user - @target = target - @project = project || default_project - @group = group || @project&.group + @custom_action = opts[:custom_action] + @acting_user = opts[:acting_user] + @target = opts[:target] + @project = opts[:project] || default_project + @group = opts[:group] || @project&.group @user = user @type = type - @skip_read_ability = skip_read_ability + @reason = opts[:reason] + @skip_read_ability = opts[:skip_read_ability] end def notification_setting @@ -76,9 +69,15 @@ class NotificationRecipient def own_activity? return false unless @acting_user - return false if @acting_user.notified_of_own_activity? - user == @acting_user + if user == @acting_user + # if activity was generated by the same user, change reason to :own_activity + @reason = NotificationReason::OWN_ACTIVITY + # If the user wants to be notified, we must return `false` + !@acting_user.notified_of_own_activity? + else + false + end end def has_access? diff --git a/app/models/project.rb b/app/models/project.rb index fbe65e700a4..0590cc1c720 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,7 +4,6 @@ class Project < ActiveRecord::Base include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Gitlab::CurrentSettings include AccessRequestable include Avatarable include CacheMarkdownField @@ -20,9 +19,9 @@ class Project < ActiveRecord::Base include GroupDescendant include Gitlab::SQL::Pattern include DeploymentPlatform + include ::Gitlab::Utils::StrongMemoize extend Gitlab::ConfigHelper - extend Gitlab::CurrentSettings BoardLimitExceeded = Class.new(StandardError) @@ -50,8 +49,8 @@ class Project < ActiveRecord::Base default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry - default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } - default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } + default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage } + default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled } default_value_for :issues_enabled, gitlab_config_features.issues default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests default_value_for :builds_enabled, gitlab_config_features.builds @@ -70,6 +69,7 @@ class Project < ActiveRecord::Base before_destroy :remove_private_deploy_keys after_destroy -> { run_after_commit { remove_pages } } + after_destroy :remove_exports after_validation :check_pending_delete @@ -179,6 +179,7 @@ class Project < ActiveRecord::Base has_many :releases has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :lfs_objects, through: :lfs_objects_projects + has_many :lfs_file_locks has_many :project_group_links has_many :invited_groups, through: :project_group_links, source: :group has_many :pages_domains @@ -198,13 +199,13 @@ class Project < ActiveRecord::Base has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :commit_statuses - has_many :pipelines, class_name: 'Ci::Pipeline' + has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project # Ci::Build objects store data on the file system such as artifact files and # build traces. Currently there's no efficient way of removing this data in # bulk that doesn't involve loading the rows into memory. As a result we're # still using `dependent: :destroy` here. - has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :runner_projects, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' @@ -233,7 +234,7 @@ class Project < ActiveRecord::Base validates :creator, presence: true, on: :create validates :description, length: { maximum: 2000 }, allow_blank: true validates :ci_config_path, - format: { without: /(\.{2}|\A\/)/, + format: { without: %r{(\.{2}|\A/)}, message: 'cannot include leading slash or directory traversal.' }, length: { maximum: 255 }, allow_blank: true @@ -245,8 +246,7 @@ class Project < ActiveRecord::Base validates :path, presence: true, project_path: true, - length: { maximum: 255 }, - uniqueness: { scope: :namespace_id } + length: { maximum: 255 } validates :namespace, presence: true validates :name, uniqueness: { scope: :namespace_id } @@ -255,17 +255,14 @@ class Project < ActiveRecord::Base validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } - validate :avatar_type, - if: ->(project) { project.avatar.present? && project.avatar_changed? } - validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validate :visibility_level_allowed_by_group validate :visibility_level_allowed_as_fork validate :check_wiki_path_conflict validates :repository_storage, presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + validates :variables, variable_duplicates: true - mount_uploader :avatar, AvatarUploader has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # Scopes @@ -288,7 +285,6 @@ class Project < ActiveRecord::Base scope :non_archived, -> { where(archived: false) } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } - scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_statistics, -> { includes(:statistics) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) } @@ -313,6 +309,7 @@ class Project < ActiveRecord::Base scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) } + scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } @@ -489,14 +486,14 @@ class Project < ActiveRecord::Base def auto_devops_enabled? if auto_devops&.enabled.nil? - current_application_settings.auto_devops_enabled? + Gitlab::CurrentSettings.auto_devops_enabled? else auto_devops.enabled? end end def has_auto_devops_implicitly_disabled? - auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled? + auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled? end def empty_repo? @@ -515,10 +512,13 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(full_path, self, disk_path: disk_path) end - def reload_repository! + def cleanup + @repository&.cleanup @repository = nil end + alias_method :reload_repository!, :cleanup + def container_registry_url if Gitlab.config.registry.enabled "#{Gitlab.config.registry.host_port}/#{full_path.downcase}" @@ -566,6 +566,9 @@ class Project < ActiveRecord::Base RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, forked_from_project.disk_path) + elsif gitlab_project_import? + # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved. + RepositoryImportWorker.set(retry: false).perform_async(self.id) else RepositoryImportWorker.perform_async(self.id) end @@ -633,6 +636,7 @@ class Project < ActiveRecord::Base project_import_data.data ||= {} project_import_data.data = project_import_data.data.merge(data) end + if credentials project_import_data.credentials ||= {} project_import_data.credentials = project_import_data.credentials.merge(credentials) @@ -917,20 +921,12 @@ class Project < ActiveRecord::Base issues_tracker.to_param == 'jira' end - def avatar_type - unless self.avatar.image? - self.errors.add :avatar, 'only images allowed' - end - end - def avatar_in_git repository.avatar end def avatar_url(**args) - # We use avatar_path instead of overriding avatar_url because of carrierwave. - # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864 - avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git) + Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git end # For compatibility with old code @@ -965,9 +961,11 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) run_after_commit_or_now do - hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend + hooks.hooks_for(hooks_scope).each do |hook| hook.async_execute(data, hooks_scope.to_s) end + + SystemHooksService.new.execute_hooks(data, hooks_scope) end end @@ -992,9 +990,13 @@ class Project < ActiveRecord::Base end def repo_exists? - @repo_exists ||= repository.exists? - rescue - @repo_exists = false + strong_memoize(:repo_exists) do + begin + repository.exists? + rescue + false + end + end end def root_ref?(branch) @@ -1023,6 +1025,8 @@ class Project < ActiveRecord::Base end def fork_source + return nil unless forked? + forked_from_project || fork_network&.root_project end @@ -1149,7 +1153,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.write_ref('HEAD', "refs/heads/#{branch}") + repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false) repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch @@ -1324,7 +1328,7 @@ class Project < ActiveRecord::Base host = "#{subdomain}.#{Settings.pages.host}".downcase # The host in URL always needs to be downcased - url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| + url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix| "#{prefix}#{subdomain}." end.downcase @@ -1429,7 +1433,7 @@ class Project < ActiveRecord::Base # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using # the import rake task. - repository.rugged.config['gitlab.fullpath'] = gl_full_path + repository.raw_repository.write_config(full_path: gl_full_path) rescue Gitlab::Git::Repository::NoRepository => e Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") nil @@ -1470,14 +1474,14 @@ class Project < ActiveRecord::Base # Ensure HEAD points to the default branch in case it is not master change_head(default_branch) - if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) + if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) params = { name: default_branch, push_access_levels_attributes: [{ - access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER }], merge_access_levels_attributes: [{ - access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER }] } @@ -1526,6 +1530,8 @@ class Project < ActiveRecord::Base end def export_path + return nil unless namespace.present? || hashed_storage?(:repository) + File.join(Gitlab::ImportExport.storage_path, disk_path) end @@ -1534,8 +1540,9 @@ class Project < ActiveRecord::Base end def remove_exports - _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) - status.zero? + return nil unless export_path.present? + + FileUtils.rm_rf(export_path) end def full_path_slug @@ -1595,7 +1602,7 @@ class Project < ActiveRecord::Base def auto_devops_variables return [] unless auto_devops_enabled? - auto_devops&.variables || [] + (auto_devops || build_auto_devops)&.variables end def append_or_update_attribute(name, value) @@ -1772,7 +1779,7 @@ class Project < ActiveRecord::Base end def use_hashed_storage - if self.new_record? && current_application_settings.hashed_storage_enabled + if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled self.storage_version = LATEST_STORAGE_VERSION end end diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 9a52edbff8e..112ed7ed434 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -6,13 +6,17 @@ class ProjectAutoDevops < ActiveRecord::Base validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } + def instance_domain + Gitlab::CurrentSettings.auto_devops_domain + end + def has_domain? - domain.present? + domain.present? || instance_domain.present? end def variables variables = [] - variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present? + variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain.presence || instance_domain, public: true } if has_domain? variables end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 9ce2d1153a7..109258d1eb7 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -84,7 +84,7 @@ http://app.asana.com/-/account_api' # - fix/ed/es/ing # - close/s/d # - closing - issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i + issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\d+/(\d+)|#(\d+))}i message.scan(issue_finder).each do |tuple| # tuple will be diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 1a236e232f9..b604d860a87 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,7 +2,7 @@ class EmailsOnPushService < Service boolean_accessor :send_from_committer_email boolean_accessor :disable_diffs prop_accessor :recipients - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? def title 'Emails on push' diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 768f0a7472e..bfe7ac29c18 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -110,6 +110,7 @@ class HipchatService < Service message = "" message << "#{push[:user_name]} " + if Gitlab::Git.blank_ref?(before) message << "pushed new #{ref_type} <a href=\""\ "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\ diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 19357f90810..27bdf708c80 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -4,7 +4,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: :activated? + validates :recipients, presence: true, if: :valid_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 31984c5d7ed..5fb15c383ca 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -10,9 +10,9 @@ class IssueTrackerService < Service # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN def self.reference_pattern(only_long: false) if only_long - %r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)} + /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/ else - %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} + /(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)/ end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2be35b6ea9d..30eafe31454 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,7 +19,7 @@ class JiraService < IssueTrackerService # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 def self.reference_pattern(only_long: true) - @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} + @reference_pattern ||= /(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)/ end def initialize_properties @@ -43,7 +43,7 @@ class JiraService < IssueTrackerService username: self.username, password: self.password, site: URI.join(url, '/').to_s, - context_path: url.path, + context_path: url.path.chomp('/'), auth_type: :basic, read_timeout: 120, use_cookies: true, diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index c72b01b64af..ad4ad7903ad 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -4,7 +4,6 @@ # After we've migrated data, we'll remove KubernetesService. This would happen in a few months. # If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes. class KubernetesService < DeploymentService - include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching @@ -151,9 +150,10 @@ class KubernetesService < DeploymentService end def deprecation_message - content = <<-MESSAGE.strip_heredoc - Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page - MESSAGE + content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % { + deprecated_message_content: deprecated_message_content, + url: Gitlab::Routing.url_helpers.project_clusters_path(project) + } content.html_safe end @@ -231,7 +231,7 @@ class KubernetesService < DeploymentService { token: token, ca_pem: ca_pem, - max_session_time: current_application_settings.terminal_max_session_time + max_session_time: Gitlab::CurrentSettings.terminal_max_session_time } end @@ -249,9 +249,9 @@ class KubernetesService < DeploymentService def deprecated_message_content if active? - "Your cluster information on this page is still editable, but you are advised to disable and reconfigure" + _("Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure") else - "Fields on this page are now uneditable, you can configure" + _("Fields on this page are now uneditable, you can configure") end end end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 6a3118a11b8..9c7b58dead5 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,7 +1,7 @@ class PipelinesEmailService < Service prop_accessor :recipients boolean_accessor :notify_only_broken_pipelines - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? def initialize_properties self.properties ||= { notify_only_broken_pipelines: true } diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index fa7b3f2bcaf..1bb576ff971 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -7,11 +7,14 @@ class PrometheusService < MonitoringService # Access to prometheus is directly through the API prop_accessor :api_url + boolean_accessor :manual_configuration - with_options presence: true, if: :activated? do + with_options presence: true, if: :manual_configuration? do validates :api_url, url: true end + before_save :synchronize_service_state! + after_save :clear_reactive_cache! def initialize_properties @@ -20,12 +23,20 @@ class PrometheusService < MonitoringService end end + def show_active_box? + false + end + + def editable? + manual_configuration? || !prometheus_installed? + end + def title 'Prometheus' end def description - s_('PrometheusService|Prometheus monitoring') + s_('PrometheusService|Time-series monitoring service') end def self.to_param @@ -33,8 +44,16 @@ class PrometheusService < MonitoringService end def fields + return [] unless editable? + [ { + type: 'checkbox', + name: 'manual_configuration', + title: s_('PrometheusService|Active'), + required: true + }, + { type: 'text', name: 'api_url', title: 'API URL', @@ -59,7 +78,7 @@ class PrometheusService < MonitoringService end def deployment_metrics(deployment) - metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics)) + metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &method(:rename_data_to_metrics)) metrics&.merge(deployment_time: deployment.created_at.to_i) || {} end @@ -68,7 +87,7 @@ class PrometheusService < MonitoringService end def additional_deployment_metrics(deployment) - with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.id, &:itself) + with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.environment.id, deployment.id, &:itself) end def matched_metrics @@ -79,6 +98,9 @@ class PrometheusService < MonitoringService def calculate_reactive_cache(query_class_name, *args) return unless active? && project && !project.pending_delete? + environment_id = args.first + client = client(environment_id) + data = Kernel.const_get(query_class_name).new(client).query(*args) { success: true, @@ -89,14 +111,55 @@ class PrometheusService < MonitoringService { success: false, result: err.message } end - def client - @prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url) + def client(environment_id = nil) + if manual_configuration? + Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url)) + else + cluster = cluster_with_prometheus(environment_id) + raise Gitlab::PrometheusError, "couldn't find cluster with Prometheus installed" unless cluster + + rest_client = client_from_cluster(cluster) + raise Gitlab::PrometheusError, "couldn't create proxy Prometheus client" unless rest_client + + Gitlab::PrometheusClient.new(rest_client) + end + end + + def prometheus_installed? + return false if template? + return false unless project + + project.clusters.enabled.any? { |cluster| cluster.application_prometheus&.installed? } end private + def cluster_with_prometheus(environment_id = nil) + clusters = if environment_id + ::Environment.find_by(id: environment_id).try do |env| + # sort results by descending order based on environment_scope being longer + # thus more closely matching environment slug + project.clusters.enabled.for_environment(env).sort_by { |c| c.environment_scope&.length }.reverse! + end + else + project.clusters.enabled.for_all_environments + end + + clusters&.detect { |cluster| cluster.application_prometheus&.installed? } + end + + def client_from_cluster(cluster) + cluster.application_prometheus.proxy_client + end + def rename_data_to_metrics(metrics) metrics[:metrics] = metrics.delete :data metrics end + + def synchronize_service_state! + self.active = prometheus_installed? || manual_configuration? + + true + end end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 17b9d2cf7b4..87a4350f022 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -37,7 +37,7 @@ class ProjectStatistics < ActiveRecord::Base def update_build_artifacts_size self.build_artifacts_size = project.builds.sum(:artifacts_size) + - Ci::JobArtifact.artifacts_size_for(self) + Ci::JobArtifact.artifacts_size_for(self.project) end def update_storage_size diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index a0af749a93f..f6041da986c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -119,12 +119,22 @@ class ProjectWiki end def delete_page(page, message = nil) + return unless page + wiki.delete_page(page.path, commit_details(:deleted, message, page.title)) update_project_activity end + def page_formatted_data(page) + page_title, page_dir = page_title_and_dir(page.title) + + wiki.page_formatted_data(title: page_title, dir: page_dir, version: page.version) + end + def page_title_and_dir(title) + return unless title + title_array = title.split("/") title = title_array.pop [title, title_array.join("/")] diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index d28fed11ca8..609780c5587 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -2,8 +2,6 @@ class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter include ProtectedRef - extend Gitlab::CurrentSettings - protected_ref_access_levels :merge, :push # Check if branch name is marked as protected in the system @@ -16,7 +14,7 @@ class ProtectedBranch < ActiveRecord::Base end def self.default_branch_protected? - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE + Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || + Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE end end diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 83ce9014094..90c085c888e 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -46,10 +46,11 @@ class PushEvent < Event # Returns PushEvent instances for which no merge requests have been created. def self.without_existing_merge_requests - existing_mrs = MergeRequest.except(:order) + existing_mrs = MergeRequest.except(:order, :where) .select(1) .where('merge_requests.source_project_id = events.project_id') .where('merge_requests.source_branch = push_event_payloads.ref') + .where(state: :opened) # For reasons unknown the use of #eager_load will result in the # "push_event_payload" association not being set. Because of this we're diff --git a/app/models/repository.rb b/app/models/repository.rb index 9c879e2006b..1cf55fd4332 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,6 +20,7 @@ class Repository attr_accessor :full_path, :disk_path, :project, :is_wiki delegate :ref_name_for_sha, to: :raw_repository + delegate :bundle_to_disk, :create_from_bundle, to: :raw_repository CreateTreeError = Class.new(StandardError) @@ -92,6 +93,10 @@ class Repository alias_method :raw, :raw_repository + def cleanup + @raw_repository&.cleanup + end + # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( @@ -103,6 +108,10 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end + def create_hooks + Gitlab::Git::Repository.create_hooks(path_to_repo, Gitlab.config.gitlab_shell.hooks_path) + end + def commit(ref = 'HEAD') return nil unless exists? return ref if ref.is_a?(::Commit) @@ -155,34 +164,27 @@ class Repository commits end + # Returns a list of commits that are not present in any reference + def new_commits(newrev) + refs = ::Gitlab::Git::RevList.new(raw, newrev: newrev).new_refs + + refs.map { |sha| commit(sha.strip) } + end + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/384 def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) unless exists? && has_visible_content? && query.present? return [] end - raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled| - commits = - if is_enabled - find_commits_by_message_by_gitaly(query, ref, path, limit, offset) - else - find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) - end - - CommitCollection.new(project, commits, ref) + commits = raw_repository.find_commits_by_message(query, ref, path, limit, offset).map do |c| + commit(c) end + CommitCollection.new(project, commits, ref) end def find_branch(name, fresh_repo: true) - # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may - # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate - # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc) - # may cause the branch to "disappear" erroneously or have the wrong SHA. - # - # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 - raw_repo = fresh_repo ? initialize_raw_repository : raw_repository - - raw_repo.find_branch(name) + raw_repository.find_branch(name, fresh_repo) end def find_tag(name) @@ -255,25 +257,15 @@ class Repository return if kept_around?(sha) # This will still fail if the file is corrupted (e.g. 0 bytes) - begin - write_ref(keep_around_ref_name(sha), sha) - rescue Rugged::ReferenceError => ex - Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" - rescue Rugged::OSError => ex - raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ - - Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" - end + raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) + rescue Gitlab::Git::CommandError => ex + Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" end def kept_around?(sha) ref_exists?(keep_around_ref_name(sha)) end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) - end - def diverging_commit_counts(branch) root_ref_hash = raw_repository.commit(root_ref).id cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -504,7 +496,7 @@ class Repository raw_repository.root_ref else # When the repo does not exist we raise this error so no data is cached. - raise Rugged::ReferenceError + raise Gitlab::Git::Repository::NoRepository end end cache_method :root_ref @@ -538,11 +530,7 @@ class Repository def commit_count_for_ref(ref) return 0 unless exists? - begin - cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) } - rescue Rugged::ReferenceError - 0 - end + cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) } end delegate :branch_names, to: :raw_repository @@ -666,26 +654,14 @@ class Repository end def last_commit_for_path(sha, path) - raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled| - if is_enabled - last_commit_for_path_by_gitaly(sha, path) - else - last_commit_for_path_by_rugged(sha, path) - end - end + commit_by(oid: last_commit_id_for_path(sha, path)) end def last_commit_id_for_path(sha, path) key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" cache.fetch(key) do - raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled| - if is_enabled - last_commit_for_path_by_gitaly(sha, path).id - else - last_commit_id_for_path_by_shelling_out(sha, path) - end - end + raw_repository.last_commit_id_for_path(sha, path) end end @@ -747,29 +723,12 @@ class Repository Commit.order_by(collection: commits, order_by: order_by, sort: sort) end - def refs_contains_sha(ref_type, sha) - args = %W(#{ref_type} --contains #{sha}) - names = run_git(args).first - - if names.respond_to?(:split) - names = names.split("\n").map(&:strip) - - names.each do |name| - name.slice! '* ' - end - - names - else - [] - end - end - def branch_names_contains(sha) - refs_contains_sha('branch', sha) + raw_repository.branch_names_contains_sha(sha) end def tag_names_contains(sha) - refs_contains_sha('tag', sha) + raw_repository.tag_names_contains_sha(sha) end def local_branches @@ -830,17 +789,6 @@ class Repository with_cache_hooks { raw.multi_action(user, **options) } end - def can_be_merged?(source_sha, target_branch) - our_commit = rugged.branches[target_branch].target - their_commit = rugged.lookup(source_sha) - - if our_commit && their_commit - !rugged.merge_commits(our_commit, their_commit).conflicts? - else - false - end - end - def merge(user, source_sha, merge_request, message) with_cache_hooks do raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id| @@ -895,52 +843,30 @@ class Repository branch = Gitlab::Git::Branch.find(self, branch_or_name) if branch - @root_ref_sha ||= commit(root_ref).sha - same_head = branch.target == @root_ref_sha - merged = ancestor?(branch.target, @root_ref_sha) + same_head = branch.target == root_ref_sha + merged = ancestor?(branch.target, root_ref_sha) !same_head && merged else nil end end - delegate :merged_branch_names, to: :raw_repository + def root_ref_sha + @root_ref_sha ||= commit(root_ref).sha + end + + delegate :merged_branch_names, :can_be_merged?, to: :raw_repository def merge_base(first_commit_id, second_commit_id) first_commit_id = commit(first_commit_id).try(:id) || first_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id raw_repository.merge_base(first_commit_id, second_commit_id) - rescue Rugged::ReferenceError - nil end def ancestor?(ancestor_id, descendant_id) return false if ancestor_id.nil? || descendant_id.nil? - Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| - if is_enabled - raw_repository.ancestor?(ancestor_id, descendant_id) - else - rugged_is_ancestor?(ancestor_id, descendant_id) - end - end - end - - def search_files_by_content(query, ref) - return [] if empty? || query.blank? - - offset = 2 - args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) - - run_git(args).first.scrub.split(/^--$/) - end - - def search_files_by_name(query, ref) - return [] if empty? || query.blank? - - args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) - - run_git(args).first.lines.map(&:strip) + raw_repository.ancestor?(ancestor_id, descendant_id) end def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil) @@ -976,6 +902,18 @@ class Repository raw_repository.ls_files(actual_ref) end + def search_files_by_content(query, ref) + return [] if empty? || query.blank? + + raw_repository.search_files_by_content(query, ref) + end + + def search_files_by_name(query, ref) + return [] if empty? + + raw_repository.search_files_by_name(query, ref) + end + def copy_gitattributes(ref) actual_ref = ref || root_ref begin @@ -1014,8 +952,9 @@ class Repository else cache.fetch(key, &block) end + instance_variable_set(ivar, value) - rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository + rescue Gitlab::Git::Repository::NoRepository # Even if the above `#exists?` check passes these errors might still # occur (for example because of a non-existing HEAD). We want to # gracefully handle this and not cache anything @@ -1109,43 +1048,7 @@ class Repository Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags)) end - def last_commit_for_path_by_gitaly(sha, path) - c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) - commit_by(oid: c) - end - - def last_commit_for_path_by_rugged(sha, path) - sha = last_commit_id_for_path_by_shelling_out(sha, path) - commit_by(oid: sha) - end - - def last_commit_id_for_path_by_shelling_out(sha, path) - args = %W(rev-list --max-count=1 #{sha} -- #{path}) - raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip - end - def initialize_raw_repository Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki)) end - - def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) - ref ||= root_ref - - args = %W( - log #{ref} --pretty=%H --skip #{offset} - --max-count #{limit} --grep=#{query} --regexp-ignore-case - ) - args = args.concat(%W(-- #{path})) if path.present? - - git_log_results = run_git(args).first.lines - - git_log_results.map { |c| commit(c.chomp) }.compact - end - - def find_commits_by_message_by_gitaly(query, ref, path, limit, offset) - raw_repository - .gitaly_commit_client - .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) - .map { |c| commit(c) } - end end diff --git a/app/models/route.rb b/app/models/route.rb index 7ba3ec06041..07d96c21cf1 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -1,4 +1,6 @@ class Route < ActiveRecord::Base + include CaseSensitivity + belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :source, presence: true @@ -8,8 +10,9 @@ class Route < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validate :ensure_permanent_paths + validate :ensure_permanent_paths, if: :path_changed? + before_validation :delete_conflicting_orphaned_routes after_create :delete_conflicting_redirects after_update :delete_conflicting_redirects, if: :path_changed? after_update :create_redirect_for_old_path @@ -72,10 +75,19 @@ class Route < ActiveRecord::Base def ensure_permanent_paths return if path.nil? - errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists? + errors.add(:path, "has been taken before") if conflicting_redirect_exists? end def conflicting_redirect_exists? RedirectRoute.permanent.matching_path_and_descendants(path).exists? end + + def delete_conflicting_orphaned_routes + conflicting = self.class.iwhere(path: path) + conflicting_orphaned_routes = conflicting.select do |route| + route.source.nil? + end + + conflicting_orphaned_routes.each(&:destroy) + end end diff --git a/app/models/service.rb b/app/models/service.rb index 24ba3039707..369cae2e85f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,6 +2,8 @@ # and implement a set of methods class Service < ActiveRecord::Base include Sortable + include Importable + serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize default_value_for :active, false @@ -118,6 +120,11 @@ class Service < ActiveRecord::Base nil end + def api_field_names + fields.map { |field| field[:name] } + .reject { |field_name| field_name =~ /(password|token|key)/ } + end + def global_fields fields end @@ -250,6 +257,7 @@ class Service < ActiveRecord::Base teamcity microsoft_teams ] + if Rails.env.development? service_names += %w[mock_ci mock_deployment mock_monitoring] end @@ -289,4 +297,8 @@ class Service < ActiveRecord::Base project.cache_has_external_wiki end end + + def valid_recipients? + activated? && !importing? + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 05a16f11b59..7c8716f8c18 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -11,8 +11,6 @@ class Snippet < ActiveRecord::Base include Editable include Gitlab::SQL::Pattern - extend Gitlab::CurrentSettings - cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description cache_markdown_field :content @@ -28,7 +26,7 @@ class Snippet < ActiveRecord::Base default_content_html_invalidator || file_name_changed? end - default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility } + default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_snippet_visibility } belongs_to :author, class_name: 'User' belongs_to :project diff --git a/app/models/todo.rb b/app/models/todo.rb index 7af54b2beb2..bb5965e20eb 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -28,6 +28,7 @@ class Todo < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true, allow_nil: true validates :action, :project, :target_type, :user, presence: true + validates :author, presence: true validates :target_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? diff --git a/app/models/upload.rb b/app/models/upload.rb index f194d7bdb80..99ad37dc892 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -9,22 +9,15 @@ class Upload < ActiveRecord::Base validates :model, presence: true validates :uploader, presence: true - before_save :calculate_checksum, if: :foreground_checksum? - after_commit :schedule_checksum, unless: :foreground_checksum? + before_save :calculate_checksum!, if: :foreground_checksummable? + after_commit :schedule_checksum, if: :checksummable? - def self.remove_path(path) - where(path: path).destroy_all - end - - def self.record(uploader) - remove_path(uploader.relative_path) + # as the FileUploader is not mounted, the default CarrierWave ActiveRecord + # hooks are not executed and the file will not be deleted + after_destroy :delete_file!, if: -> { uploader_class <= FileUploader } - create( - size: uploader.file.size, - path: uploader.relative_path, - model: uploader.model, - uploader: uploader.class.to_s - ) + def self.hexdigest(path) + Digest::SHA256.file(path).hexdigest end def absolute_path @@ -33,20 +26,47 @@ class Upload < ActiveRecord::Base uploader_class.absolute_path(self) end - def calculate_checksum - return unless exist? + def calculate_checksum! + self.checksum = nil + return unless checksummable? + + self.checksum = self.class.hexdigest(absolute_path) + end - self.checksum = Digest::SHA256.file(absolute_path).hexdigest + def build_uploader + uploader_class.new(model, mount_point, **uploader_context).tap do |uploader| + uploader.upload = self + uploader.retrieve_from_store!(identifier) + end end def exist? File.exist?(absolute_path) end + def uploader_context + { + identifier: identifier, + secret: secret + }.compact + end + private - def foreground_checksum? - size <= CHECKSUM_THRESHOLD + def delete_file! + build_uploader.remove! + end + + def checksummable? + checksum.nil? && local? && exist? + end + + def local? + true + end + + def foreground_checksummable? + checksummable? && size <= CHECKSUM_THRESHOLD end def schedule_checksum @@ -60,4 +80,12 @@ class Upload < ActiveRecord::Base def uploader_class Object.const_get(uploader) end + + def identifier + File.basename(path) + end + + def mount_point + super&.to_sym + end end diff --git a/app/models/user.rb b/app/models/user.rb index 4484ee9ff4c..05c93d3cb17 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,10 +2,8 @@ require 'carrierwave/orm/activerecord' class User < ActiveRecord::Base extend Gitlab::ConfigHelper - extend Gitlab::CurrentSettings include Gitlab::ConfigHelper - include Gitlab::CurrentSettings include Gitlab::SQL::Pattern include AfterCommitQueue include Avatarable @@ -30,7 +28,7 @@ class User < ActiveRecord::Base add_authentication_token_field :rss_token default_value_for :admin, false - default_value_for(:external) { current_application_settings.user_default_external } + default_value_for(:external) { Gitlab::CurrentSettings.user_default_external } default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false @@ -53,7 +51,10 @@ class User < ActiveRecord::Base serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize devise :lockable, :recoverable, :rememberable, :trackable, - :validatable, :omniauthable, :confirmable, :registerable + :validatable, :omniauthable, :confirmable, :registerable + + BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \ + "administrator if you think this is an error.".freeze # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour @@ -76,7 +77,7 @@ class User < ActiveRecord::Base # # Namespace for personal projects - has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent + has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent # Profile has_many :keys, -> do @@ -124,7 +125,7 @@ class User < ActiveRecord::Base has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent - has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :todos has_many :notification_settings, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent @@ -134,6 +135,8 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent has_many :custom_attributes, class_name: 'UserCustomAttribute' + has_many :callouts, class_name: 'UserCallout' + has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # # Validations @@ -148,20 +151,15 @@ class User < ActiveRecord::Base validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } - validates :username, - user_path: true, - presence: true, - uniqueness: { case_sensitive: false } + validates :username, presence: true - validate :namespace_uniq, if: :username_changed? + validates :namespace, presence: true validate :namespace_move_dir_allowed, if: :username_changed? - validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } - validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :sanitize_attrs before_validation :set_notification_email, if: :email_changed? @@ -170,7 +168,8 @@ class User < ActiveRecord::Base before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } - after_save :ensure_namespace_correct + before_validation :ensure_namespace_correct + after_validation :set_username_errors after_update :username_changed_hook, if: :username_changed? after_destroy :post_destroy_hook after_destroy :remove_key_cache @@ -217,15 +216,11 @@ class User < ActiveRecord::Base end def inactive_message - "Your account has been blocked. Please contact your GitLab " \ - "administrator if you think this is an error." + BLOCKED_MESSAGE end end end - mount_uploader :avatar, AvatarUploader - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - # Scopes scope :admins, -> { where(admin: true) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) } @@ -233,8 +228,8 @@ class User < ActiveRecord::Base scope :active, -> { with_state(:active).non_internal } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } 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('last_sign_in_at', 'DESC')) } - scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) } + 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')) } def self.with_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") @@ -316,6 +311,8 @@ class User < ActiveRecord::Base # # Returns an ActiveRecord::Relation. def search(query) + return none if query.blank? + query = query.downcase order = <<~SQL @@ -339,6 +336,8 @@ class User < ActiveRecord::Base # This method uses ILIKE on PostgreSQL and LIKE on MySQL. def search_with_secondary_emails(query) + return none if query.blank? + query = query.downcase email_table = Email.arel_table @@ -504,29 +503,12 @@ class User < ActiveRecord::Base end end - def namespace_uniq - # Return early if username already failed the first uniqueness validation - return if errors.key?(:username) && - errors[:username].include?('has already been taken') - - existing_namespace = Namespace.by_path(username) - if existing_namespace && existing_namespace != namespace - errors.add(:username, 'has already been taken') - end - end - def namespace_move_dir_allowed if namespace&.any_project_has_container_registry_tags? errors.add(:username, 'cannot be changed if a personal project has container registry tags.') end end - def avatar_type - unless avatar.image? - errors.add :avatar, "only images allowed" - end - end - def unique_email if !emails.exists?(email: email) && Email.exists?(email: email) errors.add(:email, 'has already been taken') @@ -664,11 +646,11 @@ class User < ActiveRecord::Base end def allow_password_authentication_for_web? - current_application_settings.password_authentication_enabled_for_web? && !ldap_user? + Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user? end def allow_password_authentication_for_git? - current_application_settings.password_authentication_enabled_for_git? && !ldap_user? + Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user? end def can_change_username? @@ -796,7 +778,7 @@ class User < ActiveRecord::Base # without this safeguard! return unless has_attribute?(:projects_limit) && projects_limit.nil? - self.projects_limit = current_application_settings.default_projects_limit + self.projects_limit = Gitlab::CurrentSettings.default_projects_limit end def requires_ldap_check? @@ -836,13 +818,13 @@ class User < ActiveRecord::Base end def full_website_url - return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\// + return "http://#{website_url}" if website_url !~ %r{\Ahttps?://} website_url end def short_website_url - website_url.sub(/\Ahttps?:\/\//, '') + website_url.sub(%r{\Ahttps?://}, '') end def all_ssh_keys @@ -854,9 +836,7 @@ class User < ActiveRecord::Base end def avatar_url(size: nil, scale: 2, **args) - # We use avatar_path instead of overriding avatar_url because of carrierwave. - # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864 - avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username) + GravatarService.new.execute(email, size, scale, username: username) end def primary_email_verified? @@ -891,19 +871,18 @@ class User < ActiveRecord::Base end def ensure_namespace_correct - # Ensure user has namespace - create_namespace!(path: username, name: username) unless namespace - - if username_changed? - unless namespace.update_attributes(path: username, name: username) - namespace.errors.each do |attribute, message| - self.errors.add(:"namespace_#{attribute}", message) - end - raise ActiveRecord::RecordInvalid.new(namespace) - end + if namespace + namespace.path = namespace.name = username if username_changed? + else + build_namespace(path: username, name: username) end end + def set_username_errors + namespace_path_errors = self.errors.delete(:"namespace.path") + self.errors[:username].concat(namespace_path_errors) if namespace_path_errors + end + def username_changed_hook system_hook_service.execute_hooks_for(self, :rename) end @@ -1221,7 +1200,7 @@ class User < ActiveRecord::Base else # Only revert these back to the default if they weren't specifically changed in this update. self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed? - self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed? + self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed? end end @@ -1229,15 +1208,15 @@ class User < ActiveRecord::Base valid = true error = nil - if current_application_settings.domain_blacklist_enabled? - blocked_domains = current_application_settings.domain_blacklist + if Gitlab::CurrentSettings.domain_blacklist_enabled? + blocked_domains = Gitlab::CurrentSettings.domain_blacklist if domain_matches?(blocked_domains, email) error = 'is not from an allowed domain.' valid = false end end - allowed_domains = current_application_settings.domain_whitelist + allowed_domains = Gitlab::CurrentSettings.domain_whitelist unless allowed_domains.blank? if domain_matches?(allowed_domains, email) valid = true diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb new file mode 100644 index 00000000000..e4b69382626 --- /dev/null +++ b/app/models/user_callout.rb @@ -0,0 +1,13 @@ +class UserCallout < ActiveRecord::Base + belongs_to :user + + enum feature_name: { + gke_cluster_integration: 1 + } + + validates :user, presence: true + validates :feature_name, + presence: true, + uniqueness: { scope: :user_id }, + inclusion: { in: UserCallout.feature_names.keys } +end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index bdfef677ef3..0f5536415f7 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -1,5 +1,6 @@ class WikiPage PageChangedError = Class.new(StandardError) + PageRenameError = Class.new(StandardError) include ActiveModel::Validations include ActiveModel::Conversion @@ -102,12 +103,15 @@ class WikiPage # The hierarchy of the directory this page is contained in. def directory - wiki.page_title_and_dir(slug).last + wiki.page_title_and_dir(slug)&.last.to_s end # The processed/formatted content of this page. def formatted_content - @attributes[:formatted_content] ||= @page&.formatted_data + # Assuming @page exists, nil formatted_data means we didn't load it + # before hand (i.e. page was fetched by Gitaly), so we fetch it separately. + # If the page was fetched by Gollum, formatted_data would've been a String. + @attributes[:formatted_content] ||= @page&.formatted_data || @wiki.page_formatted_data(@page) end # The markup format for the page. @@ -174,7 +178,7 @@ class WikiPage # Creates a new Wiki Page. # # attr - Hash of attributes to set on the new page. - # :title - The title for the new page. + # :title - The title (optionally including dir) for the new page. # :content - The raw markup content. # :format - Optional symbol representing the # content format. Can be any type @@ -186,7 +190,7 @@ class WikiPage # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. def create(attrs = {}) - @attributes.merge!(attrs) + update_attributes(attrs) save(page_details: title) do wiki.create_page(title, content, format, message) @@ -201,24 +205,29 @@ class WikiPage # See ProjectWiki::MARKUPS Hash for available formats. # :message - Optional commit message to set on the new version. # :last_commit_sha - Optional last commit sha to validate the page unchanged. - # :title - The Title to replace existing title + # :title - The Title (optionally including dir) to replace existing title # # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. def update(attrs = {}) last_commit_sha = attrs.delete(:last_commit_sha) + if last_commit_sha && last_commit_sha != self.last_commit_sha - raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.") + raise PageChangedError end - attrs.slice!(:content, :format, :message, :title) - @attributes.merge!(attrs) - page_details = - if title.present? && @page.title != title - title - else - @page.url_path + update_attributes(attrs) + + if title_changed? + page_details = title + + if wiki.find_page(page_details).present? + @attributes[:title] = @page.url_path + raise PageRenameError end + else + page_details = @page.url_path + end save(page_details: page_details) do wiki.update_page( @@ -252,8 +261,44 @@ class WikiPage page.version.to_s end + def title_changed? + title.present? && self.class.unhyphenize(@page.url_path) != title + end + private + # Process and format the title based on the user input. + def process_title(title) + return if title.blank? + + title = deep_title_squish(title) + current_dirname = File.dirname(title) + + if @page.present? + return title[1..-1] if current_dirname == '/' + return File.join([directory.presence, title].compact) if current_dirname == '.' + end + + title + end + + # This method squishes all the filename + # i.e: ' foo / bar / page_name' => 'foo/bar/page_name' + def deep_title_squish(title) + components = title.split(File::SEPARATOR).map(&:squish) + + File.join(components) + end + + # Updates the current @attributes hash by merging a hash of params + def update_attributes(attrs) + attrs[:title] = process_title(attrs[:title]) if attrs[:title].present? + + attrs.slice!(:content, :format, :message, :title) + + @attributes.merge!(attrs) + end + def set_attributes attributes[:slug] = @page.url_path attributes[:title] = @page.title |