diff options
Diffstat (limited to 'app/models')
53 files changed, 627 insertions, 199 deletions
diff --git a/app/models/appearance.rb b/app/models/appearance.rb index e114c435b67..ff1ecfda684 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -44,7 +44,11 @@ class Appearance < ActiveRecord::Base private def logo_system_path(logo, mount_type) - return unless logo&.upload + # Legacy attachments may not have have an associated Upload record, + # so fallback to the AttachmentUploader#url if this is the + # case. AttachmentUploader#path doesn't work because for a local + # file, this is an absolute path to the file. + return logo&.url unless logo&.upload # If we're using a CDN, we need to use the full URL asset_host = ActionController::Base.asset_host diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 655241c2808..11228e620c9 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -10,16 +10,16 @@ module BlobViewer self.file_types = %i(gitlab_ci) self.binary = false - def validation_message(project, sha) + def validation_message(opts) return @validation_message if defined?(@validation_message) prepare! - @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, { project: project, sha: sha }) + @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, opts) end - def valid?(project, sha) - validation_message(project, sha).blank? + def valid?(opts) + validation_message(opts).blank? end end end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 29aa00a66d9..5450d40ea95 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -2,11 +2,13 @@ module Ci class Bridge < CommitStatus + include Ci::Processable include Importable include AfterCommitQueue include Gitlab::Utils::StrongMemoize belongs_to :project + belongs_to :trigger_request validates :ref, presence: true def self.retry(bridge, current_user) @@ -23,6 +25,21 @@ module Ci .fabricate! end + def schedulable? + false + end + + def action? + false + end + + def artifacts? + false + end + + def expanded_environment_name + end + def predefined_variables raise NotImplementedError end @@ -30,5 +47,9 @@ module Ci def execute_hooks raise NotImplementedError end + + def to_partial_path + 'projects/generic_commit_statuses/generic_commit_status' + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index aeb35538d67..35cf4f8d277 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -3,15 +3,21 @@ module Ci class Build < CommitStatus prepend ArtifactMigratable + include Ci::Processable include TokenAuthenticatable include AfterCommitQueue include ObjectStorage::BackgroundMove include Presentable include Importable + include IgnorableColumn include Gitlab::Utils::StrongMemoize include Deployable include HasRef + BuildArchivedError = Class.new(StandardError) + + ignore_column :commands + belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request @@ -31,7 +37,7 @@ module Ci has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id end - has_one :metadata, class_name: 'Ci::BuildMetadata' + has_one :metadata, class_name: 'Ci::BuildMetadata', autosave: true has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build accepts_nested_attributes_for :runner_session @@ -219,8 +225,15 @@ module Ci before_transition any => [:failed] do |build| next unless build.project + next unless build.deployment + + begin + build.deployment.drop! + rescue => e + Gitlab::Sentry.track_exception(e, extra: { build_id: build.id }) + end - build.deployment&.drop + true end after_transition any => [:failed] do |build| @@ -273,11 +286,14 @@ module Ci # degenerated build is one that cannot be run by Runner def degenerated? - self.options.nil? + self.options.blank? end def degenerate! - self.update!(options: nil, yaml_variables: nil, commands: nil) + Build.transaction do + self.update!(options: nil, yaml_variables: nil) + self.metadata&.destroy + end end def archived? @@ -623,12 +639,20 @@ module Ci super || project.try(:build_coverage_regex) end - def when - read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' + def options + read_metadata_attribute(:options, :config_options, {}) end def yaml_variables - read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || [] + read_metadata_attribute(:yaml_variables, :config_variables, []) + end + + def options=(value) + write_metadata_attribute(:options, :config_options, value) + end + + def yaml_variables=(value) + write_metadata_attribute(:yaml_variables, :config_variables, value) end def user_variables @@ -904,8 +928,11 @@ module Ci # have the old integer only format. This method returns the retry option # normalized as a hash in 11.5+ format. def normalized_retry - value = options&.dig(:retry) - value.is_a?(Integer) ? { max: value } : value.to_h + strong_memoize(:normalized_retry) do + value = options&.dig(:retry) + value = value.is_a?(Integer) ? { max: value } : value.to_h + value.with_indifferent_access + end end def build_attributes_from_config @@ -929,5 +956,20 @@ module Ci def project_destroyed? project.pending_delete? end + + def read_metadata_attribute(legacy_key, metadata_key, default_value = nil) + read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value + end + + def write_metadata_attribute(legacy_key, metadata_key, value) + # save to metadata or this model depending on the state of feature flag + if Feature.enabled?(:ci_build_metadata_config) + ensure_metadata.write_attribute(metadata_key, value) + write_attribute(legacy_key, nil) + else + write_attribute(legacy_key, value) + metadata&.write_attribute(metadata_key, nil) + end + end end end diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 9d588b862bd..38390f49217 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -13,8 +13,12 @@ module Ci belongs_to :build, class_name: 'Ci::Build' belongs_to :project + before_create :set_build_project + validates :build, presence: true - validates :project, presence: true + + serialize :config_options, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize + serialize :config_variables, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize chronic_duration_attr_reader :timeout_human_readable, :timeout @@ -33,5 +37,11 @@ module Ci update(timeout: timeout, timeout_source: timeout_source) end + + private + + def set_build_project + self.project_id ||= self.build.project_id + end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 11c88200c37..789bb293811 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -73,6 +73,8 @@ module Ci where(file_type: types) end + scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) } + delegate :filename, :exists?, :open, to: :file enum file_type: { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1f5017cc3c3..acef5d2e643 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -25,6 +25,8 @@ module Ci has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline + has_many :processables, -> { processables }, + class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' @@ -178,6 +180,15 @@ module Ci scope :for_user, -> (user) { where(user: user) } + scope :for_merge_request, -> (merge_request, ref, sha) do + ## + # We have to filter out unrelated MR pipelines. + # When merge request is empty, it selects general pipelines, such as push sourced pipelines. + # When merge request is matched, it selects MR pipelines. + where(merge_request: [nil, merge_request], ref: ref, sha: sha) + .sort_by_merge_request_pipelines + end + # Returns the pipelines in descending order (= newest first), optionally # limited to a number of references. # @@ -265,6 +276,10 @@ module Ci sources.reject { |source| source == "external" }.values end + def self.latest_for_merge_request(merge_request, ref, sha) + for_merge_request(merge_request, ref, sha).first + end + def self.ci_sources_values config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source) end @@ -496,7 +511,7 @@ module Ci return @config_processor if defined?(@config_processor) @config_processor ||= begin - ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha }) + ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha, user: user }) rescue Gitlab::Ci::YamlProcessor::ValidationError => e self.yaml_errors = e.message nil @@ -635,7 +650,7 @@ module Ci def all_merge_requests @all_merge_requests ||= if merge_request? - project.merge_requests.where(id: merge_request.id) + project.merge_requests.where(id: merge_request_id) else project.merge_requests.where(source_branch: ref) end @@ -714,6 +729,12 @@ module Ci def git_ref if merge_request? + ## + # In the future, we're going to change this ref to + # merge request's merged reference, such as "refs/merge-requests/:iid/merge". + # In order to do that, we have to update GitLab-Runner's source pulling + # logic. + # See https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1092 Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s else super diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 8249199e76f..5aae31de6e2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -256,6 +256,10 @@ module Ci end end + def uncached_contacted_at + read_attribute(:contacted_at) + end + private def cleanup_runner_queue diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 58f3fe2460a..0389945191e 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -14,6 +14,7 @@ module Ci has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id has_many :builds, foreign_key: :stage_id + has_many :bridges, foreign_key: :stage_id with_options unless: :importing? do validates :project, presence: true diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 7799f069742..7c15aaa4825 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Ingress < ActiveRecord::Base - VERSION = '0.23.0'.freeze + VERSION = '1.1.2'.freeze self.table_name = 'clusters_applications_ingress' diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 5ac152278da..8d79b041b64 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -5,7 +5,7 @@ module Clusters class Knative < ActiveRecord::Base VERSION = '0.2.2'.freeze REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze - + METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze FETCH_IP_ADDRESS_DELAY = 30.seconds self.table_name = 'clusters_applications_knative' @@ -19,6 +19,13 @@ module Clusters self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] } + def set_initial_status + return unless not_installable? + return unless verify_cluster? + + self.status = 'installable' + end + state_machine :status do after_transition any => [:installed] do |application| application.run_after_commit do @@ -34,6 +41,8 @@ module Clusters scope :for_cluster, -> (cluster) { where(cluster: cluster) } + after_save :clear_reactive_cache! + def chart 'knative/knative' end @@ -49,7 +58,8 @@ module Clusters rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, - repository: REPOSITORY + repository: REPOSITORY, + postinstall: install_knative_metrics ) end @@ -71,7 +81,7 @@ module Clusters end def calculate_reactive_cache - { services: read_services } + { services: read_services, pods: read_pods } end def ingress_service @@ -79,7 +89,7 @@ module Clusters end def services_for(ns: namespace) - return unless services + return [] unless services return [] unless ns services.select do |service| @@ -87,13 +97,35 @@ module Clusters end end + def service_pod_details(ns, service) + with_reactive_cache do |data| + data[:pods].select { |pod| filter_pods(pod, ns, service) } + end + end + private + def read_pods + cluster.kubeclient.core_client.get_pods.as_json + end + + def filter_pods(pod, namespace, service) + pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service + end + def read_services client.get_services.as_json rescue Kubeclient::ResourceNotFoundError [] end + + def install_knative_metrics + ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available? + end + + def verify_cluster? + cluster&.application_helm_available? && cluster&.platform_kubernetes_rbac? + end end end end diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 46d0388a464..26bf73f4dd8 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -5,7 +5,8 @@ module Clusters class Prometheus < ActiveRecord::Base include PrometheusAdapter - VERSION = '6.7.3'.freeze + VERSION = '6.7.3' + READY_STATUS = [:installed, :updating, :updated, :update_errored].freeze self.table_name = 'clusters_applications_prometheus' @@ -24,12 +25,8 @@ module Clusters end end - def ready_status - [:installed] - end - def ready? - ready_status.include?(status_name) + READY_STATUS.include?(status_name) end def chart @@ -50,10 +47,29 @@ module Clusters version: VERSION, rbac: cluster.platform_kubernetes_rbac?, chart: chart, - files: files + files: files, + postinstall: install_knative_metrics + ) + end + + def upgrade_command(values) + ::Gitlab::Kubernetes::Helm::UpgradeCommand.new( + name, + version: VERSION, + chart: chart, + rbac: cluster.platform_kubernetes_rbac?, + files: files_with_replaced_values(values) ) end + # Returns a copy of files where the values of 'values.yaml' + # are replaced by the argument. + # + # See #values for the data format required + def files_with_replaced_values(replaced_values) + files.merge('values.yaml': replaced_values) + end + def prometheus_client return unless kube_client @@ -74,6 +90,10 @@ module Clusters def kube_client cluster&.kubeclient&.core_client end + + def install_knative_metrics + ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available? + end end end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 0c0247da1fb..f17da0bb7b1 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ActiveRecord::Base - VERSION = '0.1.43'.freeze + VERSION = '0.1.45'.freeze self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7fe43cd2de0..a2c48973fa5 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -49,8 +49,9 @@ module Clusters validates :name, cluster_name: true validates :cluster_type, presence: true - validate :restrict_modification, on: :update + validates :domain, allow_nil: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } + validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? validate :no_projects, unless: :project_type? @@ -63,6 +64,7 @@ module Clusters delegate :available?, to: :application_helm, prefix: true, allow_nil: true delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true + delegate :available?, to: :application_knative, prefix: true, allow_nil: true enum cluster_type: { instance_type: 1, diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 0e74cce29b7..a556dd5ad8b 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -77,6 +77,10 @@ module Clusters def available? installed? || updated? end + + def update_in_progress? + updating? + end end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 0dc0c4f80d6..8f3424db295 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -65,6 +65,8 @@ module Clusters abac: 2 } + default_value_for :authorization_type, :rbac + def actual_namespace if namespace.present? namespace @@ -152,7 +154,7 @@ module Clusters def build_kube_client! raise "Incomplete settings" unless api_url - raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace + raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace unless (username && password) || token raise "Either username/password or token is required to access API" diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb index e349f0fe971..885f61beb05 100644 --- a/app/models/commit_collection.rb +++ b/app/models/commit_collection.rb @@ -19,6 +19,12 @@ class CommitCollection commits.each(&block) end + def committers + emails = commits.reject(&:merge_commit?).map(&:committer_email).uniq + + User.by_any_email(emails) + end + # Sets the pipeline status for every commit. # # Setting this status ahead of time removes the need for running a query for diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 0f50bd39131..7f6562b63e5 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -41,6 +41,7 @@ class CommitStatus < ActiveRecord::Base scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } + scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) } # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily # extend this `Hash` with new values. diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index a8c9e54f00c..73a27326f6c 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -15,7 +15,7 @@ module CacheMarkdownField # Increment this number every time the renderer changes its output CACHE_REDCARPET_VERSION = 3 CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 12 + CACHE_COMMONMARK_VERSION = 13 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/concerns/ci/processable.rb b/app/models/concerns/ci/processable.rb new file mode 100644 index 00000000000..1c78b1413a8 --- /dev/null +++ b/app/models/concerns/ci/processable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Ci + ## + # This module implements methods that need to be implemented by CI/CD + # entities that are supposed to go through pipeline processing + # services. + # + # + module Processable + def schedulable? + raise NotImplementedError + end + + def action? + raise NotImplementedError + end + + def when + read_attribute(:when) || 'on_success' + end + + def expanded_environment_name + raise NotImplementedError + end + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index b92643f87f8..0d2be4c61ab 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -85,11 +85,11 @@ module HasStatus scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } - scope :failed, -> { where(status: 'failed') } - scope :canceled, -> { where(status: 'canceled') } - scope :skipped, -> { where(status: 'skipped') } - scope :manual, -> { where(status: 'manual') } - scope :scheduled, -> { where(status: 'scheduled') } + scope :failed, -> { where(status: 'failed') } + scope :canceled, -> { where(status: 'canceled') } + scope :skipped, -> { where(status: 'skipped') } + scope :manual, -> { where(status: 'manual') } + scope :scheduled, -> { where(status: 'scheduled') } scope :alive, -> { where(status: [:created, :pending, :running]) } scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb index e18edd33ba7..ff61412767e 100644 --- a/app/models/concerns/manual_inverse_association.rb +++ b/app/models/concerns/manual_inverse_association.rb @@ -5,8 +5,8 @@ module ManualInverseAssociation class_methods do def manual_inverse_association(association, inverse) - define_method(association) do |*args| - super(*args).tap do |value| + define_method(association) do + super().tap do |value| next unless value child_association = value.association(inverse) diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 2c08a8e1acf..cf057d774cf 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ContainerRepository < ActiveRecord::Base + include Gitlab::Utils::StrongMemoize + belongs_to :project validates :name, length: { minimum: 0, allow_nil: false } @@ -8,6 +10,8 @@ class ContainerRepository < ActiveRecord::Base delegate :client, to: :registry + scope :ordered, -> { order(:name) } + # rubocop: disable CodeReuse/ServiceClass def registry @registry ||= begin @@ -39,11 +43,12 @@ class ContainerRepository < ActiveRecord::Base end def tags - return @tags if defined?(@tags) return [] unless manifest && manifest['tags'] - @tags = manifest['tags'].map do |tag| - ContainerRegistry::Tag.new(self, tag) + strong_memoize(:tags) do + manifest['tags'].sort.map do |tag| + ContainerRegistry::Tag.new(self, tag) + end end end diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb index 9bcc95e35a5..74aa04ab7d0 100644 --- a/app/models/dashboard_group_milestone.rb +++ b/app/models/dashboard_group_milestone.rb @@ -11,11 +11,12 @@ class DashboardGroupMilestone < GlobalMilestone @group_name = milestone.group.full_name end - def self.build_collection(groups) - Milestone.of_groups(groups.select(:id)) + def self.build_collection(groups, params) + milestones = Milestone.of_groups(groups.select(:id)) .reorder_by_due_date_asc .order_by_name_asc .active - .map { |m| new(m) } + milestones = milestones.search_title(params[:search_title]) if params[:search_title].present? + milestones.map { |m| new(m) } end end diff --git a/app/models/email.rb b/app/models/email.rb index b6a977dfa22..3ce6e792fa8 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -15,7 +15,7 @@ class Email < ActiveRecord::Base after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') } devise :confirmable - self.reconfirmable = false # currently email can't be changed, no need to reconfirm + self.reconfirmable = false # currently email can't be changed, no need to reconfirm delegate :username, to: :user diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb new file mode 100644 index 00000000000..7f4947ba27a --- /dev/null +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module ErrorTracking + class ProjectErrorTrackingSetting < ActiveRecord::Base + include ReactiveCaching + + self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } + + belongs_to :project + + validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true } + + validate :validate_api_url_path + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' + + after_save :clear_reactive_cache! + + def sentry_client + Sentry::Client.new(api_url, token) + end + + def sentry_external_url + self.class.extract_sentry_external_url(api_url) + end + + def list_sentry_issues(opts = {}) + with_reactive_cache('list_issues', opts.stringify_keys) do |result| + { issues: result } + end + end + + def calculate_reactive_cache(request, opts) + case request + when 'list_issues' + sentry_client.list_issues(**opts.symbolize_keys) + end + end + + # http://HOST/api/0/projects/ORG/PROJECT + # -> + # http://HOST/ORG/PROJECT + def self.extract_sentry_external_url(url) + url.sub('api/0/projects/', '') + end + + private + + def validate_api_url_path + unless URI(api_url).path.starts_with?('/api/0/projects') + errors.add(:api_url, 'path needs to start with /api/0/projects') + end + rescue URI::InvalidURIError + end + end +end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 4f73beaafc5..68b2353556e 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -3,6 +3,8 @@ class ExternalIssue include Referable + attr_reader :project + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -32,12 +34,8 @@ class ExternalIssue [self.class, to_s].hash end - def project - @project - end - def project_id - @project.id + project.id end def to_reference(_from = nil, full: nil) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 4e82f3fed27..fd17745b035 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -27,6 +27,7 @@ class GlobalMilestone items = Milestone.of_projects(projects) .reorder_by_due_date_asc .order_by_name_asc + items = items.search_title(params[:search_title]) if params[:search_title].present? Milestone.filter_by_state(items, params[:state]).map { |m| new(m) } end diff --git a/app/models/group.rb b/app/models/group.rb index edac2444c4d..52f503404af 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -98,7 +98,7 @@ class Group < Namespace def select_for_project_authorization if current_scope.joins_values.include?(:shared_projects) joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') - .where('project_namespace.share_with_group_lock = ?', false) + .where('project_namespace.share_with_group_lock = ?', false) .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") else super @@ -382,6 +382,10 @@ class Group < Namespace end end + def highest_group_member(user) + GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last + end + def hashed_storage?(_feature) false end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index a58537de319..97cb26c6ea9 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -5,9 +5,10 @@ class GroupMilestone < GlobalMilestone def self.build_collection(group, projects, params) params = - { state: params[:state] } + { state: params[:state], search_title: params[:search_title] } project_milestones = Milestone.of_projects(projects) + project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present? child_milestones = Milestone.filter_by_state(project_milestones, params[:state]) grouped_milestones = child_milestones.group_by(&:title) diff --git a/app/models/issue.rb b/app/models/issue.rb index b7e13bcbccf..5c4ecbfdf4e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -230,7 +230,8 @@ class Issue < ActiveRecord::Base end def check_for_spam? - project.public? && (title_changed? || description_changed?) + publicly_visible? && + (title_changed? || description_changed? || confidential_changed?) end def as_json(options = {}) diff --git a/app/models/label.rb b/app/models/label.rb index 5d2d1afd1d9..1c3db3eb35d 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -214,6 +214,7 @@ class Label < ActiveRecord::Base super(options).tap do |json| json[:type] = self.try(:type) json[:priority] = priority(options[:project]) if options.key?(:project) + json[:textColor] = text_color end end diff --git a/app/models/list.rb b/app/models/list.rb index 029685be927..682af761ba0 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -54,6 +54,6 @@ class List < ActiveRecord::Base private def can_be_destroyed - destroyable? + throw(:abort) unless destroyable? end end diff --git a/app/models/member.rb b/app/models/member.rb index 9fc95ea00c3..b0f049438eb 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -76,7 +76,7 @@ class Member < ActiveRecord::Base scope :maintainers, -> { active.where(access_level: MAINTAINER) } scope :masters, -> { maintainers } # @deprecated scope :owners, -> { active.where(access_level: OWNER) } - scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } + scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6092c56b925..7206d858dae 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -284,6 +284,14 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end + def committers + @committers ||= commits.committers + end + + def authors + User.from_union([committers, User.where(id: self.author_id)]) + end + # Verifies if title has changed not taking into account WIP prefix # for merge requests. def wipless_title_changed(old_title) @@ -327,13 +335,15 @@ class MergeRequest < ActiveRecord::Base end def commits - if persisted? - merge_request_diff.commits - elsif compare_commits - compare_commits.reverse - else - [] - end + return merge_request_diff.commits if persisted? + + commits_arr = if compare_commits + compare_commits.reverse + else + [] + end + + CommitCollection.new(source_project, commits_arr, source_branch) end def commits_count @@ -550,15 +560,19 @@ class MergeRequest < ActiveRecord::Base end def diff_refs - if persisted? - merge_request_diff.diff_refs - else - Gitlab::Diff::DiffRefs.new( - base_sha: diff_base_sha, - start_sha: diff_start_sha, - head_sha: diff_head_sha - ) - end + persisted? ? merge_request_diff.diff_refs : repository_diff_refs + end + + # Instead trying to fetch the + # persisted diff_refs, this method goes + # straight to the repository to get the + # most recent data possible. + def repository_diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: branch_merge_base_sha, + start_sha: target_branch_sha, + head_sha: source_branch_sha + ) end def branch_merge_base_sha @@ -1092,10 +1106,16 @@ class MergeRequest < ActiveRecord::Base def all_pipelines(shas: all_commit_shas) return Ci::Pipeline.none unless source_project - @all_pipelines ||= source_project.ci_pipelines - .where(sha: shas, ref: source_branch) - .where(merge_request: [nil, self]) - .sort_by_merge_request_pipelines + @all_pipelines ||= + source_project.ci_pipelines + .for_merge_request(self, source_branch, all_commit_shas) + end + + def update_head_pipeline + find_actual_head_pipeline.try do |pipeline| + self.head_pipeline = pipeline + update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed? + end end def merge_request_pipeline_exists? @@ -1338,4 +1358,11 @@ class MergeRequest < ActiveRecord::Base source_project.repository.squash_in_progress?(id) end + + private + + def find_actual_head_pipeline + source_project&.ci_pipelines + &.latest_for_merge_request(self, source_branch, diff_head_sha) + end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index f55c39d9912..26cfdc5ef30 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -28,7 +28,7 @@ class Milestone < ActiveRecord::Base has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) } has_many :issues - has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues + has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -38,12 +38,14 @@ class Milestone < ActiveRecord::Base scope :closed, -> { with_state(:closed) } scope :for_projects, -> { where(group: nil).includes(:project) } - scope :for_projects_and_groups, -> (project_ids, group_ids) do - conditions = [] - conditions << arel_table[:project_id].in(project_ids) if project_ids&.compact&.any? - conditions << arel_table[:group_id].in(group_ids) if group_ids&.compact&.any? + scope :for_projects_and_groups, -> (projects, groups) do + projects = projects.compact if projects.is_a? Array + projects = [] if projects.nil? - where(conditions.reduce(:or)) + groups = groups.compact if groups.is_a? Array + groups = [] if groups.nil? + + where(project_id: projects).or(where(group_id: groups)) end scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } @@ -75,7 +77,7 @@ class Milestone < ActiveRecord::Base alias_attribute :name, :title class << self - # Searches for milestones matching the given query. + # Searches for milestones with a matching title or description. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. # @@ -86,6 +88,17 @@ class Milestone < ActiveRecord::Base fuzzy_search(query, [:title, :description]) end + # Searches for milestones with a matching title. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. + def search_title(query) + fuzzy_search(query, [:title]) + end + def filter_by_state(milestones, state) case state when 'closed' then milestones.closed @@ -133,18 +146,29 @@ class Milestone < ActiveRecord::Base @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/) end - def self.upcoming_ids_by_projects(projects) - rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) + def self.upcoming_ids(projects, groups) + rel = unscoped + .for_projects_and_groups(projects, groups) + .active.where('milestones.due_date > NOW()') if Gitlab::Database.postgresql? - rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') + rel.order(:project_id, :group_id, :due_date).select('DISTINCT ON (project_id, group_id) id') else + # We need to use MySQL's NULL-safe comparison operator `<=>` here + # because one of `project_id` or `group_id` is always NULL + join_clause = <<~HEREDOC + LEFT OUTER JOIN milestones earlier_milestones + ON milestones.project_id <=> earlier_milestones.project_id + AND milestones.group_id <=> earlier_milestones.group_id + AND milestones.due_date > earlier_milestones.due_date + AND earlier_milestones.due_date > NOW() + AND earlier_milestones.state = 'active' + HEREDOC + rel - .group(:project_id, :due_date, :id) - .having('due_date = MIN(due_date)') - .pluck(:id, :project_id, :due_date) - .uniq(&:second) - .map(&:first) + .joins(join_clause) + .where('earlier_milestones.id IS NULL') + .select(:id) end end @@ -178,7 +202,7 @@ class Milestone < ActiveRecord::Base return STATE_COUNT_HASH unless projects || groups counts = Milestone - .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .for_projects_and_groups(projects, groups) .reorder(nil) .group(:state) .count @@ -262,8 +286,7 @@ class Milestone < ActiveRecord::Base if project relation = Milestone.for_projects_and_groups([project_id], [project.group&.id]) elsif group - project_ids = group.projects.map(&:id) - relation = Milestone.for_projects_and_groups(project_ids, [group.id]) + relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id]) end title_exists = relation.find_by_title(title) diff --git a/app/models/note.rb b/app/models/note.rb index becf14e9785..1578ae9c4cc 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -456,6 +456,10 @@ class Note < ActiveRecord::Base Upload.find_by(model: self, path: paths) end + def parent + project + end + private def keep_around_commit diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index ad6a008dee8..34220c1b450 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -85,7 +85,11 @@ class PoolRepository < ActiveRecord::Base def unlink_repository(repository) object_pool.unlink_repository(repository.raw) - mark_obsolete unless member_projects.where.not(id: repository.project.id).exists? + if member_projects.where.not(id: repository.project.id).exists? + true + else + mark_obsolete + end end def object_pool diff --git a/app/models/project.rb b/app/models/project.rb index cd558752080..15465d9b356 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -73,7 +73,7 @@ class Project < ActiveRecord::Base delegate :no_import?, to: :import_state, allow_nil: true default_value_for :archived, false - default_value_for :visibility_level, gitlab_config_features.visibility_level + default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility } default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage } @@ -187,6 +187,7 @@ class Project < ActiveRecord::Base has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :project_repository, inverse_of: :project + has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting' # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project @@ -295,6 +296,8 @@ class Project < ActiveRecord::Base allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } + accepts_nested_attributes_for :error_tracking_setting, update_only: true + delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team @@ -328,10 +331,10 @@ class Project < ActiveRecord::Base ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, enforce_user: true }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } - validate :check_limit, on: :create + validate :check_personal_projects_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } - validate :visibility_level_allowed_by_group - validate :visibility_level_allowed_as_fork + validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) } + validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) } validate :check_wiki_path_conflict validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) } validates :repository_storage, @@ -644,19 +647,15 @@ class Project < ActiveRecord::Base end # ref can't be HEAD, can only be branch/tag name or SHA - def latest_successful_builds_for(ref = default_branch) + def latest_successful_build_for(job_name, ref = default_branch) latest_pipeline = ci_pipelines.latest_successful_for(ref) + return unless latest_pipeline - if latest_pipeline - latest_pipeline.builds.latest.with_artifacts_archive - else - builds.none - end + latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name) end - def latest_successful_build_for(job_name, ref = default_branch) - builds = latest_successful_builds_for(ref) - builds.find_by!(name: job_name) + def latest_successful_build_for!(job_name, ref = default_branch) + latest_successful_build_for(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}")) end def merge_base_commit(first_commit_id, second_commit_id) @@ -810,18 +809,22 @@ class Project < ActiveRecord::Base ::Gitlab::CurrentSettings.mirror_available end - def check_limit - unless creator.can_create_project? || namespace.kind == 'group' - projects_limit = creator.projects_limit + def check_personal_projects_limit + # Since this method is called as validation hook, `creator` might not be + # present. Since the validation for that will fail, we can just return + # early. + return if !creator || creator.can_create_project? || + namespace.kind == 'group' - if projects_limit == 0 - self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions") + limit = creator.projects_limit + error = + if limit.zero? + _('Personal project creation is not allowed. Please contact your administrator with questions') else - self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it") + _('Your project limit is %{limit} projects! Please contact your administrator to increase it') end - end - rescue - self.errors.add(:base, "Can't check your ability to create project") + + self.errors.add(:limit_reached, error % { limit: limit }) end def visibility_level_allowed_by_group @@ -912,11 +915,16 @@ class Project < ActiveRecord::Base def new_issuable_address(author, address_type) return unless Gitlab::IncomingEmail.supports_issue_creation? && author + # check since this can come from a request parameter + return unless %w(issue merge_request).include?(address_type) + author.ensure_incoming_email_token! - suffix = address_type == 'merge_request' ? '+merge-request' : '' - Gitlab::IncomingEmail.reply_address( - "#{full_path}#{suffix}+#{author.incoming_email_token}") + suffix = address_type.dasherize + + # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue@localhost.com + # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-merge-request@localhost.com + Gitlab::IncomingEmail.reply_address("#{full_path_slug}-#{project_id}-#{author.incoming_email_token}-#{suffix}") end def build_commit_note(commit) @@ -1527,7 +1535,7 @@ class Project < ActiveRecord::Base end def pages_available? - Gitlab.config.pages.enabled && !namespace.subgroup? + Gitlab.config.pages.enabled end def remove_private_deploy_keys @@ -1598,24 +1606,7 @@ class Project < ActiveRecord::Base # rubocop: disable CodeReuse/ServiceClass def after_create_default_branch - return unless default_branch - - # Ensure HEAD points to the default branch in case it is not master - change_head(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: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }], - merge_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }] - } - - ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true) - end + Projects::ProtectDefaultBranchService.new(self).execute end # rubocop: enable CodeReuse/ServiceClass @@ -1700,6 +1691,13 @@ class Project < ActiveRecord::Base .append(key: 'CI_PROJECT_VISIBILITY', value: visibility) .concat(container_registry_variables) .concat(auto_devops_variables) + .concat(api_variables) + end + + def api_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url) + end end def container_registry_variables @@ -1774,6 +1772,24 @@ class Project < ActiveRecord::Base handle_update_attribute_error(e, value) end + # Tries to set repository as read_only, checking for existing Git transfers in progress beforehand + # + # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress + def set_repository_read_only! + with_lock do + break false if git_transfer_in_progress? + + update_column(:repository_read_only, true) + end + end + + # Set repository as writable again + def set_repository_writable! + with_lock do + update_column(repository_read_only, false) + end + end + def pushes_since_gc Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } end @@ -1888,15 +1904,17 @@ class Project < ActiveRecord::Base def migrate_to_hashed_storage! return unless storage_upgradable? - update!(repository_read_only: true) - - if repo_reference_count > 0 || wiki_reference_count > 0 + if git_transfer_in_progress? ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id) else ProjectMigrateHashedStorageWorker.perform_async(id) end end + def git_transfer_in_progress? + repo_reference_count > 0 || wiki_reference_count > 0 + end + def storage_version=(value) super @@ -1928,23 +1946,15 @@ class Project < ActiveRecord::Base .where('project_authorizations.project_id = merge_requests.target_project_id') .limit(1) .select(1) - source_of_merge_requests.opened - .where(allow_collaboration: true) - .where('EXISTS (?)', developer_access_exists) + merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists) end - def branch_allows_collaboration?(user, branch_name) - return false unless user - - cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push" - - memoized_results = strong_memoize(:branch_allows_collaboration) do - Hash.new do |result, cache_key| - result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name) - end - end + def any_branch_allows_collaboration?(user) + fetch_branch_allows_collaboration(user) + end - memoized_results[cache_key] + def branch_allows_collaboration?(user, branch_name) + fetch_branch_allows_collaboration(user, branch_name) end def licensed_features @@ -2013,11 +2023,21 @@ class Project < ActiveRecord::Base end def leave_pool_repository - pool_repository&.unlink_repository(repository) + pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil) + end + + def link_pool_repository + pool_repository&.link_repository(repository) end private + def merge_requests_allowing_collaboration(source_branch = nil) + relation = source_of_merge_requests.opened.where(allow_collaboration: true) + relation = relation.where(source_branch: source_branch) if source_branch + relation + end + def create_new_pool_repository pool = begin create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self) @@ -2142,26 +2162,19 @@ class Project < ActiveRecord::Base raise ex end - def fetch_branch_allows_collaboration?(user, branch_name) - check_access = -> do - next false if empty_repo? + def fetch_branch_allows_collaboration(user, branch_name = nil) + return false unless user - merge_requests = source_of_merge_requests.opened - .where(allow_collaboration: true) + Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do + next false if empty_repo? # Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322 Gitlab::GitalyClient.allow_n_plus_1_calls do - if branch_name - merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user) - else - merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) } + merge_requests_allowing_collaboration(branch_name).any? do |merge_request| + merge_request.can_be_merged_by?(user) end end end - - Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do - check_access.call - end end def services_templates diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 525725034a5..aa0c121fe99 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -30,4 +30,8 @@ class ProjectImportData < ActiveRecord::Base def merge_credentials(hash) self.credentials = credentials.to_h.merge(hash) unless hash.empty? end + + def clear_credentials + self.credentials = {} + end end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index a15780c14f9..83fd9a34438 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -59,7 +59,7 @@ class IrkerService < Service ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \ ' want to use a password, you have to omit the "#" on the channel). If you ' \ ' specify a default IRC URI to prepend before each recipient, you can just ' \ - ' give a channel name.' }, + ' give a channel name.' }, { type: 'checkbox', name: 'colorize_messages' } ] end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b8e17087db5..3245cd22e73 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -39,9 +39,7 @@ class TeamcityService < CiService end def help - 'The build configuration in Teamcity must use the build format '\ - 'number %build.vcs.number% '\ - 'you will also want to configure monitoring of all branches so merge '\ + 'You will want to configure monitoring of all branches so merge '\ 'requests build, that setting is in the vsc root advanced settings.' end @@ -70,7 +68,7 @@ class TeamcityService < CiService end def calculate_reactive_cache(sha, ref) - response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") + response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}") { build_page: read_build_page(response), commit_status: read_commit_status(response) } end diff --git a/app/models/release.rb b/app/models/release.rb index df3dfe1cf2f..0dae5c90394 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -10,6 +10,10 @@ class Release < ActiveRecord::Base # releases prior to 11.7 have no author belongs_to :author, class_name: 'User' + has_many :links, class_name: 'Releases::Link' + + accepts_nested_attributes_for :links, allow_destroy: true + validates :description, :project, :tag, presence: true scope :sorted, -> { order(created_at: :desc) } @@ -26,6 +30,16 @@ class Release < ActiveRecord::Base actual_tag.nil? end + def assets_count + links.count + sources.count + end + + def sources + strong_memoize(:sources) do + Releases::Source.all(project, tag) + end + end + private def actual_sha diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb new file mode 100644 index 00000000000..6f639e5a7b2 --- /dev/null +++ b/app/models/releases/link.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Releases + class Link < ActiveRecord::Base + self.table_name = 'release_links' + + belongs_to :release + + validates :url, presence: true, url: true, uniqueness: { scope: :release } + validates :name, presence: true, uniqueness: { scope: :release } + + scope :sorted, -> { order(created_at: :desc) } + + def internal? + url.start_with?(release.project.web_url) + end + + def external? + !internal? + end + end +end diff --git a/app/models/releases/source.rb b/app/models/releases/source.rb new file mode 100644 index 00000000000..4d3d54457af --- /dev/null +++ b/app/models/releases/source.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Releases + class Source + include ActiveModel::Model + + attr_accessor :project, :tag_name, :format + + FORMATS = %w(zip tar.gz tar.bz2 tar).freeze + + class << self + def all(project, tag_name) + Releases::Source::FORMATS.map do |format| + Releases::Source.new(project: project, + tag_name: tag_name, + format: format) + end + end + end + + def url + Gitlab::Routing + .url_helpers + .project_archive_url(project, + id: File.join(tag_name, archive_prefix), + format: format) + end + + private + + def archive_prefix + "#{project.path}-#{tag_name.tr('/', '-')}" + end + end +end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index a3fa67c72bf..5eba7ddd75c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -61,7 +61,10 @@ class RemoteMirror < ActiveRecord::Base timestamp = Time.now remote_mirror.update!( - last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil + last_update_at: timestamp, + last_successful_update_at: timestamp, + last_error: nil, + error_notification_sent: false ) end @@ -179,6 +182,10 @@ class RemoteMirror < ActiveRecord::Base project.repository.add_remote(remote_name, remote_url) end + def after_sent_notification + update_column(:error_notification_sent, true) + end + private def store_credentials @@ -221,7 +228,8 @@ class RemoteMirror < ActiveRecord::Base last_error: nil, last_update_at: nil, last_successful_update_at: nil, - update_status: 'finished' + update_status: 'finished', + error_notification_sent: false ) end diff --git a/app/models/repository.rb b/app/models/repository.rb index b19ae2e0e6a..b47238b52f1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1072,19 +1072,11 @@ class Repository end def cache - @cache ||= if is_wiki - Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki') - else - Gitlab::RepositoryCache.new(self) - end + @cache ||= Gitlab::RepositoryCache.new(self) end def request_store_cache - @request_store_cache ||= if is_wiki - Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki', backend: Gitlab::SafeRequestStore) - else - Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) - end + @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end def tags_sorted_by_committed_date diff --git a/app/models/snippet.rb b/app/models/snippet.rb index f9b23bbbf6c..f23ddd64fe3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -50,11 +50,11 @@ class Snippet < ActiveRecord::Base validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } # Scopes - scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } + scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) } scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } - scope :fresh, -> { order("created_at DESC") } + scope :fresh, -> { order("created_at DESC") } scope :inc_relations_for_view, -> { includes(author: :status) } participant :author diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb index b6844dbe870..99a0c54a26a 100644 --- a/app/models/ssh_host_key.rb +++ b/app/models/ssh_host_key.rb @@ -52,6 +52,11 @@ class SshHostKey @compare_host_keys = compare_host_keys end + # Needed for reactive caching + def self.primary_key + 'id' + end + def id [project.id, url].join(':') end diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index 911fb7e9ce9..f5d0d6fab3b 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -31,7 +31,7 @@ module Storage gitlab_shell.add_namespace(repository_storage, base_dir) end - def rename_repo + def rename_repo(old_full_path: nil, new_full_path: nil) true end diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 9f6f19acb41..76ac5c13c18 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -29,18 +29,19 @@ module Storage gitlab_shell.add_namespace(repository_storage, base_dir) end - def rename_repo - new_full_path = project.build_full_path + def rename_repo(old_full_path: nil, new_full_path: nil) + old_full_path ||= project.full_path_was + new_full_path ||= project.build_full_path - if gitlab_shell.mv_repository(repository_storage, project.full_path_was, new_full_path) + if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path) # If repository moved successfully we need to send update instructions to users. # However we cannot allow rollback since we moved repository # So we basically we mute exceptions in next actions begin - gitlab_shell.mv_repository(repository_storage, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki") + gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki") return true rescue => e - Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}" + Rails.logger.error "Exception renaming #{old_full_path} -> #{new_full_path}: #{e}" # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks return false diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index c76b8e71507..7eee4fbbe5f 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -5,8 +5,7 @@ class Suggestion < ApplicationRecord validates :note, presence: true validates :commit_id, presence: true, if: :applied? - delegate :original_position, :position, :diff_file, - :noteable, to: :note + delegate :original_position, :position, :noteable, to: :note def project noteable.source_project @@ -16,6 +15,15 @@ class Suggestion < ApplicationRecord noteable.source_branch end + def file_path + position.file_path + end + + def diff_file + repository = project.repository + position.diff_file(repository) + end + # For now, suggestions only serve as a way to send patches that # will change a single line (being able to apply multiple in the same place), # which explains `from_line` and `to_line` being the same line. diff --git a/app/models/user.rb b/app/models/user.rb index 26fd2d903a1..4ef5bdc2d12 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -145,7 +145,7 @@ class User < ActiveRecord::Base has_many :issue_assignees has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue - has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent + 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' |