diff options
Diffstat (limited to 'app/models')
26 files changed, 323 insertions, 76 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 80bda7f22ff..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 } diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 78906e7a968..ee987949080 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -21,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 @@ -542,6 +543,7 @@ module Ci variables = [ { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, 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 f84bf132854..2abe90dd181 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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 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/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 7f38dcc4a9c..7ce8befeeeb 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -180,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/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/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/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/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index b12c10a84de..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 diff --git a/app/models/group.rb b/app/models/group.rb index 5b7f1b38612..75bf013ecd2 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -31,9 +31,12 @@ class Group < Namespace 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 :variables, variable_duplicates: true validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } 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 d025062f562..5bec68ce4f6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -158,10 +158,12 @@ class MergeRequest < ActiveRecord::Base end def rebase_in_progress? - # The source project can be deleted - return false unless source_project + strong_memoize(:rebase_in_progress) do + # The source project can be deleted + next false unless source_project - source_project.repository.rebase_in_progress?(id) + source_project.repository.rebase_in_progress?(id) + end end # Use this method whenever you need to make sure the head_pipeline is synced with the diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 69a846da9be..c1c27ccf3e5 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -290,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 7b82d076975..db274ea8172 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -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 @@ -222,9 +223,12 @@ class Namespace < ActiveRecord::Base end def full_path_was - return path_was unless has_parent? - - "#{parent.full_path}/#{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 @@ -239,8 +243,16 @@ class Namespace < ActiveRecord::Base all_projects.with_storage_feature(:repository).find_each(&:remove_exports) end + def features + [] + 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) @@ -271,16 +283,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/project.rb b/app/models/project.rb index 12d5f28f5ea..3893b1818f3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 @@ -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 } @@ -261,6 +261,7 @@ class Project < ActiveRecord::Base validates :repository_storage, presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + validates :variables, variable_duplicates: true has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -511,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}" @@ -1585,8 +1589,11 @@ class Project < ActiveRecord::Base end def protected_for?(ref) - ProtectedBranch.protected?(self, ref) || + if repository.branch_exists?(ref) + ProtectedBranch.protected?(self, ref) + elsif repository.tag_exists?(ref) ProtectedTag.protected?(self, ref) + end end def deployment_variables @@ -1598,7 +1605,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) 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/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index e42fd802b92..ad4ad7903ad 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -150,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 @@ -248,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/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/repository.rb b/app/models/repository.rb index f1abe5c3e07..1cf55fd4332 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -93,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( @@ -160,6 +164,13 @@ 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? diff --git a/app/models/route.rb b/app/models/route.rb index 3d4b5a8b5ee..07d96c21cf1 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -75,7 +75,7 @@ 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? diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 7c8716f8c18..a58c208279e 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -74,6 +74,27 @@ class Snippet < ActiveRecord::Base @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end + # Returns a collection of snippets that are either public or visible to the + # logged in user. + # + # This method does not verify the user actually has the access to the project + # the snippet is in, so it should be only used on a relation that's already scoped + # for project access + def self.public_or_visible_to_user(user = nil) + if user + authorized = user + .project_authorizations + .select(1) + .where('project_authorizations.project_id = snippets.project_id') + + levels = Gitlab::VisibilityLevel.levels_for_user(user) + + where('EXISTS (?) OR snippets.visibility_level IN (?) or snippets.author_id = (?)', authorized, levels, user.id) + else + public_to_user + end + end + def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{id}" diff --git a/app/models/upload.rb b/app/models/upload.rb index 2024228537a..99ad37dc892 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -12,6 +12,10 @@ class Upload < ActiveRecord::Base before_save :calculate_checksum!, if: :foreground_checksummable? after_commit :schedule_checksum, if: :checksummable? + # 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 } + def self.hexdigest(path) Digest::SHA256.file(path).hexdigest end @@ -49,6 +53,10 @@ class Upload < ActiveRecord::Base private + def delete_file! + build_uploader.remove! + end + def checksummable? checksum.nil? && local? && exist? end diff --git a/app/models/user.rb b/app/models/user.rb index 4b44e9bb7eb..4097fe2b5dc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -77,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 @@ -151,12 +151,9 @@ 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 :unique_email, if: :email_changed? @@ -171,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 @@ -230,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") @@ -505,17 +503,6 @@ 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.') @@ -564,7 +551,7 @@ class User < ActiveRecord::Base gpg_keys.each(&:update_invalid_gpg_signatures) end - # Returns the groups a user has access to + # Returns the groups a user has access to, either through a membership or a project authorization def authorized_groups union = Gitlab::SQL::Union .new([groups.select(:id), authorized_projects.select(:namespace_id)]) @@ -572,6 +559,11 @@ class User < ActiveRecord::Base Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end + # Returns the groups a user is a member of, either directly or through a parent group + def membership_groups + Gitlab::GroupHierarchy.new(groups).base_and_descendants + end + # Returns a relation of groups the user has access to, including their parent # and child groups (recursively). def all_expanded_groups @@ -884,19 +876,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 |