diff options
author | Simon Knox <psimyn@gmail.com> | 2017-09-06 14:35:58 +1000 |
---|---|---|
committer | Simon Knox <psimyn@gmail.com> | 2017-09-06 14:35:58 +1000 |
commit | b9aa55e1ea2ba226bd9bf4c6fb08fdec30e046c5 (patch) | |
tree | b7770180f178086c78ef2ca25d6bb2267f739110 /app/models | |
parent | 74740604211dab6632771f1bfd7dd67902fea7ef (diff) | |
parent | d68ff7f50a93ebbff537b5e795cf6bf80bd66a6e (diff) | |
download | gitlab-ce-b9aa55e1ea2ba226bd9bf4c6fb08fdec30e046c5.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ee_issue_928_backport
Diffstat (limited to 'app/models')
37 files changed, 395 insertions, 174 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 8e446ff6dd8..3568e72e463 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -13,6 +13,11 @@ class ApplicationSetting < ActiveRecord::Base [\r\n] # any number of newline characters }x + # Setting a key restriction to `-1` means that all keys of this type are + # forbidden. + FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN + SUPPORTED_KEY_TYPES = %i[rsa dsa ecdsa ed25519].freeze + serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize @@ -146,6 +151,12 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { greater_than_or_equal_to: 0 } + SUPPORTED_KEY_TYPES.each do |type| + validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } + end + + validates :allowed_key_types, presence: true + validates_each :restricted_visibility_levels do |record, attr, value| value&.each do |level| unless Gitlab::VisibilityLevel.options.value?(level) @@ -171,6 +182,7 @@ class ApplicationSetting < ActiveRecord::Base end before_validation :ensure_uuid! + before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token @@ -221,6 +233,9 @@ class ApplicationSetting < ActiveRecord::Base default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], disabled_oauth_sign_in_sources: [], domain_whitelist: Settings.gitlab['domain_whitelist'], + dsa_key_restriction: 0, + ecdsa_key_restriction: 0, + ed25519_key_restriction: 0, gravatar_enabled: Settings.gravatar['enabled'], help_page_text: nil, help_page_hide_commercial_content: false, @@ -239,6 +254,7 @@ class ApplicationSetting < ActiveRecord::Base max_attachment_size: Settings.gitlab['max_attachment_size'], password_authentication_enabled: Settings.gitlab['password_authentication_enabled'], performance_bar_allowed_group_id: nil, + rsa_key_restriction: 0, plantuml_enabled: false, plantuml_url: nil, project_export_enabled: true, @@ -413,6 +429,18 @@ class ApplicationSetting < ActiveRecord::Base usage_ping_can_be_configured? && super end + def allowed_key_types + SUPPORTED_KEY_TYPES.select do |type| + key_restriction_for(type) != FORBIDDEN_KEY_VALUE + end + end + + def key_restriction_for(type) + attr_name = "#{type}_key_restriction" + + has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend + end + private def ensure_uuid! diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 91b62dabbcd..4d1a15c53aa 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -17,6 +17,9 @@ class AwardEmoji < ActiveRecord::Base scope :downvotes, -> { where(name: DOWNVOTE_NAME) } scope :upvotes, -> { where(name: UPVOTE_NAME) } + after_save :expire_etag_cache + after_destroy :expire_etag_cache + class << self def votes_for_collection(ids, type) select('name', 'awardable_id', 'COUNT(*) as count') @@ -32,4 +35,8 @@ class AwardEmoji < ActiveRecord::Base def upvote? self.name == UPVOTE_NAME end + + def expire_etag_cache + awardable.try(:expire_etag_cache) + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 095192e9894..ba3156154ac 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -3,6 +3,7 @@ module Ci include TokenAuthenticatable include AfterCommitQueue include Presentable + include Importable belongs_to :runner belongs_to :trigger_request @@ -26,6 +27,7 @@ module Ci validates :coverage, numericality: true, allow_blank: true validates :ref, presence: true + validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } @@ -34,6 +36,7 @@ module Ci scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } + scope :ref_protected, -> { where(protected: true) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -387,7 +390,9 @@ module Ci [ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }, + { key: 'GITLAB_USER_LOGIN', value: user.username, public: true }, + { key: 'GITLAB_USER_NAME', value: user.name, public: true } ] end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2d40f8012a3..35d14b6e297 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -36,6 +36,7 @@ module Ci validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :status, presence: { unless: :importing? } + validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create validate :valid_commit_sha, unless: :importing? after_create :keep_around_commits, unless: :importing? @@ -304,6 +305,10 @@ module Ci @stage_seeds ||= config_processor.stage_seeds(self) end + def has_kubernetes_active? + project.kubernetes_service&.active? + end + def has_stage_seeds? stage_seeds.any? end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index c6d23898560..b1798084787 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -5,7 +5,7 @@ module Ci RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour AVAILABLE_SCOPES = %w[specific shared active paused online].freeze - FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze + FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze has_many :builds has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -35,11 +35,17 @@ module Ci end validate :tag_constraints + validates :access_level, presence: true acts_as_taggable after_destroy :cleanup_runner_queue + enum access_level: { + not_protected: 0, + ref_protected: 1 + } + # Searches for runners matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -106,6 +112,8 @@ module Ci end def can_pick?(build) + return false if self.ref_protected? && !build.protected? + assignable_for?(build.project) && accepting_tags?(build) end @@ -142,7 +150,7 @@ module Ci expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false) end - def is_runner_queue_value_latest?(value) + def runner_queue_value_latest?(value) ensure_runner_queue_value == value if value.present? end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index c58ce5c3717..2c860598281 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -6,6 +6,10 @@ module Ci belongs_to :pipeline, foreign_key: :commit_id has_many :builds + # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables. + # Ci::TriggerRequest doesn't save variables anymore. + validates :variables, absence: true + serialize :variables # rubocop:disable Cop/ActiveRecordSerialize def user_variables diff --git a/app/models/commit.rb b/app/models/commit.rb index d41c88b4e30..ba3845df867 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -251,6 +251,28 @@ class Commit project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end + def cherry_pick_description(user) + message_body = "(cherry picked from commit #{sha})" + + if merged_merge_request?(user) + commits_in_merge_request = merged_merge_request(user).commits + + if commits_in_merge_request.present? + message_body << "\n" + + commits_in_merge_request.reverse.each do |commit_in_merge| + message_body << "\n#{commit_in_merge.short_id} #{commit_in_merge.title}" + end + end + end + + message_body + end + + def cherry_pick_message(user) + %Q{#{message}\n\n#{cherry_pick_description(user)}} + end + def revert_description(user) if merged_merge_request?(user) "This reverts merge request #{merged_merge_request(user).to_reference}" @@ -383,6 +405,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) + @gpg_commit ||= Gitlab::Gpg::Commit.new(self) end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 842c6e5cb50..f3888528940 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } + enum failure_reason: { + unknown_failure: nil, + script_failure: 1, + api_failure: 2, + stuck_or_timeout_failure: 3, + runner_system_failure: 4 + } + state_machine :status do event :process do transition [:skipped, :manual] => :created @@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base commit_status.finished_at = Time.now end + before_transition any => :failed do |commit_status, transition| + failure_reason = transition.args.first + commit_status.failure_reason = failure_reason + end + after_transition do |commit_status, transition| next if transition.loopback? diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index f4f9b037957..9adc309a22b 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -11,6 +11,21 @@ module Awardable end module ClassMethods + def awarded(user, name) + sql = <<~EOL + EXISTS ( + SELECT TRUE + FROM award_emoji + WHERE user_id = :user_id AND + name = :name AND + awardable_type = :awardable_type AND + awardable_id = #{self.arel_table.name}.id + ) + EOL + + where(sql, user_id: user.id, name: name, awardable_type: self.name) + end + def order_upvotes_desc order_votes_desc(AwardEmoji::UPVOTE_NAME) end diff --git a/app/models/concerns/editable.rb b/app/models/concerns/editable.rb index 28623d257a6..c0a3099f676 100644 --- a/app/models/concerns/editable.rb +++ b/app/models/concerns/editable.rb @@ -1,7 +1,7 @@ module Editable extend ActiveSupport::Concern - def is_edited? + def edited? last_edited_at.present? && last_edited_at != created_at end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3731b7c8577..681c3241dbb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,6 +6,7 @@ # module Issuable extend ActiveSupport::Concern + include Gitlab::SQL::Pattern include CacheMarkdownField include Participable include Mentionable @@ -122,7 +123,9 @@ module Issuable # # Returns an ActiveRecord::Relation. def search(query) - where(arel_table[:title].matches("%#{query}%")) + title = to_fuzzy_arel(:title, query) + + where(title) end # Searches for records with a matching title or description. @@ -133,10 +136,10 @@ module Issuable # # Returns an ActiveRecord::Relation. def full_search(query) - t = arel_table - pattern = "%#{query}%" + title = to_fuzzy_arel(:title, query) + description = to_fuzzy_arel(:description, query) - where(t[:title].matches(pattern).or(t[:description].matches(pattern))) + where(title&.or(description)) end def sort(method, excluded_labels: []) diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index f0998465822..710fc1ed647 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -70,19 +70,19 @@ module Milestoneish due_date && due_date.past? end - def is_group_milestone? + def group_milestone? false end - def is_project_milestone? + def project_milestone? false end - def is_legacy_group_milestone? + def legacy_group_milestone? false end - def is_dashboard_milestone? + def dashboard_milestone? false end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index c7bdc997eca..1c4ddabcad5 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -24,6 +24,10 @@ module Noteable DiscussionNote::NOTEABLE_TYPES.include?(base_class_name) end + def discussions_rendered_on_frontend? + false + end + def discussion_notes notes end @@ -38,7 +42,7 @@ module Noteable def grouped_diff_discussions(*args) # Doesn't use `discussion_notes`, because this may include commit diff notes - # besides MR diff notes, that we do no want to display on the MR Changes tab. + # besides MR diff notes, that we do not want to display on the MR Changes tab. notes.inc_relations_for_view.grouped_diff_discussions(*args) end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index f2707022a4b..731d9b9a745 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -28,7 +28,7 @@ module Spammable def submittable_as_spam? if user_agent_detail - user_agent_detail.submittable? && current_application_settings.akismet_enabled + user_agent_detail.submittable? && Gitlab::CurrentSettings.current_application_settings.akismet_enabled else false end diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb index fac7c5e5c85..86eb4ec76fc 100644 --- a/app/models/dashboard_milestone.rb +++ b/app/models/dashboard_milestone.rb @@ -3,7 +3,7 @@ class DashboardMilestone < GlobalMilestone { authorized_only: true } end - def is_dashboard_milestone? + def dashboard_milestone? true end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 056c49e7162..7bcded5b5e1 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -49,7 +49,7 @@ class Deployment < ActiveRecord::Base # 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.is_ancestor?(commit.id, sha) + project.repository.ancestor?(commit.id, sha) rescue Rugged::OdbError false end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index d1cec7613af..b80da7b246a 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -81,6 +81,10 @@ class Discussion last_note.author end + def updated? + last_updated_at != created_at + end + def id first_note.discussion_id(context_noteable) end diff --git a/app/models/environment.rb b/app/models/environment.rb index e9ebf0637f3..435eeaf0e2e 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -114,7 +114,7 @@ class Environment < ActiveRecord::Base end def ref_path - "refs/environments/#{Shellwords.shellescape(name)}" + "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" end def formatted_external_url diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 3df60ddc950..1633acd4fa9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base def verified_user_infos user_infos.select do |user_info| - user_info[:email] == user.email + user.verified_email?(user_info[:email]) end end @@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base user_infos.map do |user_info| [ user_info[:email], - user_info[:email] == user.email + user.verified_email?(user_info[:email]) ] end.to_h end def verified? - emails_with_verified_status.any? { |_email, verified| verified } + emails_with_verified_status.values.any? + end + + def verified_and_belongs_to_email?(email) + emails_with_verified_status.fetch(email, false) end def update_invalid_gpg_signatures @@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base end def revoke - GpgSignature.where(gpg_key: self, valid_signature: true).update_all( - gpg_key_id: nil, - valid_signature: false, - updated_at: Time.zone.now - ) + GpgSignature + .where(gpg_key: self) + .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key]) + .update_all( + gpg_key_id: nil, + verification_status: GpgSignature.verification_statuses[:unknown_key], + updated_at: Time.zone.now + ) destroy end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 50fb35c77ec..454c90d5fc4 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,9 +1,21 @@ class GpgSignature < ActiveRecord::Base include ShaAttribute + include IgnorableColumn + + ignore_column :valid_signature sha_attribute :commit_sha sha_attribute :gpg_key_primary_keyid + enum verification_status: { + unverified: 0, + verified: 1, + same_user_different_email: 2, + other_user: 3, + unverified_key: 4, + unknown_key: 5 + } + belongs_to :project belongs_to :gpg_key @@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base end def gpg_commit - Gitlab::Gpg::Commit.new(project, commit_sha) + Gitlab::Gpg::Commit.new(commit) end end diff --git a/app/models/group.rb b/app/models/group.rb index cb3ee032f69..190b27cf66b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -26,6 +26,8 @@ class Group < Namespace validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } 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 } @@ -102,15 +104,24 @@ class Group < Namespace full_name end - def visibility_level_allowed_by_projects - allowed_by_projects = self.projects.where('visibility_level > ?', self.visibility_level).none? + def visibility_level_allowed_by_parent?(level = self.visibility_level) + return true unless parent_id && parent_id.nonzero? - unless allowed_by_projects - level_name = Gitlab::VisibilityLevel.level_name(visibility_level).downcase - self.errors.add(:visibility_level, "#{level_name} is not allowed since there are projects with higher visibility.") - end + level <= parent.visibility_level + end + + def visibility_level_allowed_by_projects?(level = self.visibility_level) + !projects.where('visibility_level > ?', level).exists? + end - allowed_by_projects + def visibility_level_allowed_by_sub_groups?(level = self.visibility_level) + !children.where('visibility_level > ?', level).exists? + end + + def visibility_level_allowed?(level = self.visibility_level) + visibility_level_allowed_by_parent?(level) && + visibility_level_allowed_by_projects?(level) && + visibility_level_allowed_by_sub_groups?(level) end def avatar_url(**args) @@ -275,11 +286,29 @@ class Group < Namespace list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten end - protected + private def update_two_factor_requirement return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed? users.find_each(&:update_two_factor_requirement) end + + def visibility_level_allowed_by_parent + return if visibility_level_allowed_by_parent? + + errors.add(:visibility_level, "#{visibility} is not allowed since the parent group has a #{parent.visibility} visibility.") + end + + def visibility_level_allowed_by_projects + return if visibility_level_allowed_by_projects? + + errors.add(:visibility_level, "#{visibility} is not allowed since this group contains projects with higher visibility.") + end + + def visibility_level_allowed_by_sub_groups + return if visibility_level_allowed_by_sub_groups? + + errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.") + end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 65249bd7bfc..98135ee3c8b 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -17,7 +17,7 @@ class GroupMilestone < GlobalMilestone { group_id: group.id } end - def is_legacy_group_milestone? + def legacy_group_milestone? true end end diff --git a/app/models/issue.rb b/app/models/issue.rb index b9aa937d2f9..8c7d492e605 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -269,7 +269,17 @@ class Issue < ActiveRecord::Base end end + def discussions_rendered_on_frontend? + true + end + + def update_project_counter_caches? + state_changed? || confidential_changed? + end + def update_project_counter_caches + return unless update_project_counter_caches? + Projects::OpenIssuesCountService.new(project).refresh_cache end diff --git a/app/models/key.rb b/app/models/key.rb index 49bc26122fa..a6b4dcfec0d 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -1,6 +1,7 @@ require 'digest/md5' class Key < ActiveRecord::Base + include Gitlab::CurrentSettings include Sortable LAST_USED_AT_REFRESH_TIME = 1.day.to_i @@ -12,14 +13,18 @@ class Key < ActiveRecord::Base validates :title, presence: true, length: { maximum: 255 } + validates :key, presence: true, length: { maximum: 5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } + validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } + validate :key_meets_restrictions + delegate :name, :email, to: :user, prefix: true after_commit :add_to_shell, on: :create @@ -80,6 +85,10 @@ class Key < ActiveRecord::Base SystemHooksService.new.execute_hooks_for(self, :destroy) end + def public_key + @public_key ||= Gitlab::SSHPublicKey.new(key) + end + private def generate_fingerprint @@ -87,7 +96,27 @@ class Key < ActiveRecord::Base return unless self.key.present? - self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint + self.fingerprint = public_key.fingerprint + end + + def key_meets_restrictions + restriction = current_application_settings.key_restriction_for(public_key.type) + + if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE + errors.add(:key, forbidden_key_type_message) + elsif public_key.bits < restriction + errors.add(:key, "must be at least #{restriction} bits") + end + end + + def forbidden_key_type_message + allowed_types = + current_application_settings + .allowed_key_types + .map(&:upcase) + .to_sentence(last_word_connector: ', or ', two_words_connector: ' or ') + + "type is forbidden. Must be #{allowed_types}" end def notify_user diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index dbc73ed3cd4..724fb4ccef1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -241,6 +241,14 @@ class MergeRequest < ActiveRecord::Base end end + # Calls `MergeWorker` to proceed with the merge process and + # updates `merge_jid` with the MergeWorker#jid. + # This helps tracking enqueued and ongoing merge jobs. + def merge_async(user_id, params) + jid = MergeWorker.perform_async(id, user_id, params) + update_column(:merge_jid, jid) + end + def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end @@ -384,9 +392,7 @@ class MergeRequest < ActiveRecord::Base end def merge_ongoing? - return false unless merge_jid - - Gitlab::SidekiqStatus.num_running([merge_jid]) > 0 + !!merge_jid && !merged? end def closed_without_fork? @@ -599,6 +605,8 @@ class MergeRequest < ActiveRecord::Base self.merge_requests_closing_issues.delete_all closes_issues(current_user).each do |issue| + next if issue.is_a?(ExternalIssue) + self.merge_requests_closing_issues.create!(issue: issue) end end @@ -797,7 +805,7 @@ class MergeRequest < ActiveRecord::Base end def ref_path - "refs/merge-requests/#{iid}/head" + "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" end def ref_fetched? @@ -819,7 +827,7 @@ class MergeRequest < ActiveRecord::Base lock_mr yield ensure - unlock_mr if locked? + unlock_mr end end @@ -936,20 +944,19 @@ class MergeRequest < ActiveRecord::Base true end + def update_project_counter_caches? + state_changed? + end + def update_project_counter_caches + return unless update_project_counter_caches? + Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache end private def write_ref - target_project.repository.with_repo_branch_commit( - source_project.repository, source_branch) do |commit| - if commit - target_project.repository.write_ref(ref_path, commit.sha) - else - raise Rugged::ReferenceError, 'source repository is empty' - end - end + target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path) end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 01e0d0155a3..a3070a12b7c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -163,7 +163,7 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid, full: false) - return if is_group_milestone? && format != :name + return if group_milestone? && format != :name format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" @@ -207,11 +207,11 @@ class Milestone < ActiveRecord::Base group || project end - def is_group_milestone? + def group_milestone? group_id.present? end - def is_project_milestone? + def project_milestone? project_id.present? end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e7bc1d1b080..e7cbc5170e8 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -195,6 +195,10 @@ class Namespace < ActiveRecord::Base parent.present? end + def subgroup? + 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. diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 0e5acb22d50..3845e485413 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -152,14 +152,14 @@ module Network end def find_free_parent_space(range, space_base, space_step, space_default) - if is_overlap?(range, space_default) + if overlap?(range, space_default) find_free_space(range, space_step, space_base, space_default) else space_default end end - def is_overlap?(range, overlap_space) + def overlap?(range, overlap_space) range.each do |i| if i != range.first && i != range.last && diff --git a/app/models/note.rb b/app/models/note.rb index a752c897d63..1073c115630 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -299,6 +299,17 @@ class Note < ActiveRecord::Base end end + def expire_etag_cache + return unless noteable&.discussions_rendered_on_frontend? + + key = Gitlab::Routing.url_helpers.project_noteable_notes_path( + project, + target_type: noteable_type.underscore, + target_id: noteable_id + ) + Gitlab::EtagCaching::Store.new.touch(key) + end + private def keep_around_commit @@ -326,15 +337,4 @@ class Note < ActiveRecord::Base def set_discussion_id self.discussion_id ||= discussion_class.discussion_id(self) end - - def expire_etag_cache - return unless for_issue? - - key = Gitlab::Routing.url_helpers.project_noteable_notes_path( - noteable.project, - target_type: noteable_type.underscore, - target_id: noteable.id - ) - Gitlab::EtagCaching::Store.new.touch(key) - end end diff --git a/app/models/project.rb b/app/models/project.rb index 8ade8c3fc38..01d04bc8d04 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -19,6 +19,7 @@ class Project < ActiveRecord::Base include Routable extend Gitlab::ConfigHelper + extend Gitlab::CurrentSettings BoardLimitExceeded = Class.new(StandardError) @@ -67,7 +68,6 @@ class Project < ActiveRecord::Base acts_as_taggable - attr_accessor :new_default_branch attr_accessor :old_path_with_namespace attr_accessor :template_name attr_writer :pipeline_status @@ -222,6 +222,7 @@ class Project < ActiveRecord::Base validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create + validate :can_create_repository?, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? } validate :avatar_type, if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } @@ -372,11 +373,7 @@ class Project < ActiveRecord::Base if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? project.run_after_commit do - begin - Projects::HousekeepingService.new(project).execute - rescue Projects::HousekeepingService::LeaseTaken => e - Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}") - end + Projects::AfterImportService.new(project).execute end end end @@ -468,7 +465,7 @@ class Project < ActiveRecord::Base end def repository_storage_path - Gitlab.config.repositories.storages[repository_storage]['path'] + Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') end def team @@ -583,7 +580,7 @@ class Project < ActiveRecord::Base end def valid_import_url? - valid? || errors.messages[:import_url].nil? + valid?(:import_url) || errors.messages[:import_url].nil? end def create_or_update_import_data(data: nil, credentials: nil) @@ -1000,6 +997,20 @@ class Project < ActiveRecord::Base end end + # Check if repository already exists on disk + def can_create_repository? + return false unless repository_storage_path + + expires_full_path_cache # we need to clear cache to validate renames correctly + + if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") + errors.add(:base, 'There is already a repository with that name on disk') + return false + end + + true + end + def create_repository(force: false) # Forked import is handled asynchronously return if forked? && !force @@ -1235,6 +1246,10 @@ class Project < ActiveRecord::Base File.join(pages_path, 'public') end + def pages_available? + Gitlab.config.pages.enabled && !namespace.subgroup? + end + def remove_private_deploy_keys exclude_keys_linked_to_other_projects = <<-SQL NOT EXISTS ( @@ -1494,6 +1509,10 @@ class Project < ActiveRecord::Base self.storage_version.nil? end + def renamed? + persisted? && path_changed? + end + private def storage diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 7b15a5dd04d..818cfb01b14 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -101,9 +101,9 @@ class ChatNotificationService < Service when "push", "tag_push" ChatMessage::PushMessage.new(data) when "issue" - ChatMessage::IssueMessage.new(data) unless is_update?(data) + ChatMessage::IssueMessage.new(data) unless update?(data) when "merge_request" - ChatMessage::MergeMessage.new(data) unless is_update?(data) + ChatMessage::MergeMessage.new(data) unless update?(data) when "note" ChatMessage::NoteMessage.new(data) when "pipeline" @@ -136,7 +136,7 @@ class ChatNotificationService < Service project.web_url end - def is_update?(data) + def update?(data) data[:object_attributes][:action] == 'update' end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index f422e0ea036..976d85246a8 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -85,9 +85,9 @@ class HipchatService < Service when "push", "tag_push" create_push_message(data) when "issue" - create_issue_message(data) unless is_update?(data) + create_issue_message(data) unless update?(data) when "merge_request" - create_merge_request_message(data) unless is_update?(data) + create_merge_request_message(data) unless update?(data) when "note" create_note_message(data) when "pipeline" @@ -282,7 +282,7 @@ class HipchatService < Service "<a href=\"#{project_url}\">#{project_name}</a>" end - def is_update?(data) + def update?(data) data[:object_attributes][:action] == 'update' end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 5f0d0802ac9..89bfc5f9a9c 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -2,6 +2,8 @@ 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 diff --git a/app/models/repository.rb b/app/models/repository.rb index 9fb2e2aa306..035f85a0b46 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,6 +1,18 @@ require 'securerandom' class Repository + REF_MERGE_REQUEST = 'merge-requests'.freeze + REF_KEEP_AROUND = 'keep-around'.freeze + REF_ENVIRONMENTS = 'environments'.freeze + + RESERVED_REFS_NAMES = %W[ + heads + tags + #{REF_ENVIRONMENTS} + #{REF_KEEP_AROUND} + #{REF_ENVIRONMENTS} + ].freeze + include Gitlab::ShellAdapter include RepositoryMirroring @@ -8,7 +20,6 @@ class Repository delegate :ref_name_for_sha, to: :raw_repository - CommitError = Class.new(StandardError) CreateTreeError = Class.new(StandardError) # Methods that cache data from the Git repository. @@ -60,6 +71,10 @@ class Repository @project = project end + def ==(other) + @disk_path == other.disk_path + end + def raw_repository return nil unless full_path @@ -75,17 +90,8 @@ class Repository ) end - # - # Git repository can contains some hidden refs like: - # /refs/notes/* - # /refs/git-as-svn/* - # /refs/pulls/* - # This refs by default not visible in project page and not cloned to client side. - # - # This method return true if repository contains some content visible in project page. - # - def has_visible_content? - branch_count > 0 + def inspect + "#<#{self.class.name}:#{@disk_path}>" end def commit(ref = 'HEAD') @@ -160,32 +166,25 @@ class Repository end def add_branch(user, branch_name, ref) - newrev = commit(ref).try(:sha) - - return false unless newrev - - GitOperationService.new(user, self).add_branch(branch_name, newrev) + branch = raw_repository.add_branch(branch_name, committer: user, target: ref) after_create_branch - find_branch(branch_name) + + branch + rescue Gitlab::Git::Repository::InvalidRef + false end def add_tag(user, tag_name, target, message = nil) - newrev = commit(target).try(:id) - options = { message: message, tagger: user_to_committer(user) } if message - - return false unless newrev - - GitOperationService.new(user, self).add_tag(tag_name, newrev, options) - - find_tag(tag_name) + raw_repository.add_tag(tag_name, committer: user, target: target, message: message) + rescue Gitlab::Git::Repository::InvalidRef + false end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - GitOperationService.new(user, self).rm_branch(branch) + raw_repository.rm_branch(branch_name, committer: user) after_remove_branch true @@ -193,9 +192,8 @@ class Repository def rm_tag(user, tag_name) before_remove_tag - tag = find_tag(tag_name) - GitOperationService.new(user, self).rm_tag(tag) + raw_repository.rm_tag(tag_name, committer: user) after_remove_tag true @@ -234,10 +232,10 @@ class Repository begin write_ref(keep_around_ref_name(sha), sha) rescue Rugged::ReferenceError => ex - Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{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 keep-around reference for repository #{path}: #{ex}" + Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" end end @@ -764,16 +762,30 @@ class Repository multi_action(**options) end + def with_branch(user, *args) + result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit| + yield start_commit + end + + newrev, should_run_after_create, should_run_after_create_branch = result + + after_create if should_run_after_create + after_create_branch if should_run_after_create_branch + + newrev + end + # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| index = Gitlab::Git::Index.new(raw_repository) @@ -826,7 +838,8 @@ class Repository end def merge(user, source, merge_request, options = {}) - GitOperationService.new(user, self).with_branch( + with_branch( + user, merge_request.target_branch) do |start_commit| our_commit = start_commit.sha their_commit = source @@ -846,17 +859,18 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end - rescue Repository::CommitError # when merge_index.conflicts? + rescue Gitlab::Git::CommitError # when merge_index.conflicts? false end def revert( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| revert_tree_id = check_revert_content(commit, start_commit.sha) unless revert_tree_id @@ -876,10 +890,11 @@ class Repository def cherry_pick( user, commit, branch_name, start_branch_name: nil, start_project: project) - GitOperationService.new(user, self).with_branch( + with_branch( + user, branch_name, start_branch_name: start_branch_name, - start_project: start_project) do |start_commit| + start_repository: start_project.repository.raw_repository) do |start_commit| cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) unless cherry_pick_tree_id @@ -888,7 +903,7 @@ class Repository committer = user_to_committer(user) - create_commit(message: commit.message, + create_commit(message: commit.cherry_pick_message(user), author: { email: commit.author_email, name: commit.author_name, @@ -901,7 +916,7 @@ class Repository end def resolve_conflicts(user, branch_name, params) - GitOperationService.new(user, self).with_branch(branch_name) do + with_branch(user, branch_name) do committer = user_to_committer(user) create_commit(params.merge(author: committer, committer: committer)) @@ -944,7 +959,7 @@ class Repository if branch_commit same_head = branch_commit.id == root_ref_commit.id - !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id) + !same_head && ancestor?(branch_commit.id, root_ref_commit.id) else nil end @@ -958,12 +973,12 @@ class Repository nil end - def is_ancestor?(ancestor_id, descendant_id) + 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.is_ancestor?(ancestor_id, descendant_id) + raw_repository.ancestor?(ancestor_id, descendant_id) else rugged_is_ancestor?(ancestor_id, descendant_id) end @@ -991,28 +1006,6 @@ class Repository run_git(args).first.lines.map(&:strip) end - def with_repo_branch_commit(start_repository, start_branch_name) - return yield(nil) if start_repository.empty_repo? - - branch_name_or_sha = - if start_repository == self - start_branch_name - else - tmp_ref = fetch_ref( - start_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" - ) - - start_repository.commit(start_branch_name).sha - end - - yield(commit(branch_name_or_sha)) - - ensure - rugged.references.delete(tmp_ref) if tmp_ref - end - def add_remote(name, url) raw_repository.remote_add(name, url) rescue Rugged::ConfigError @@ -1027,17 +1020,15 @@ class Repository end def fetch_remote(remote, forced: false, no_tags: false) - gitlab_shell.fetch_remote(repository_storage_path, disk_path, remote, forced: forced, no_tags: no_tags) + gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) - - # Make sure ref was created, and raise Rugged::ReferenceError when not - raise Rugged::ReferenceError, message if status != 0 + def fetch_source_branch(source_repository, source_branch, local_ref) + raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref) + end - target_ref + def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) + raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight) end def create_ref(ref, ref_path) @@ -1118,12 +1109,6 @@ class Repository private - def run_git(args) - circuit_breaker.perform do - Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo) - end - end - def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1159,7 +1144,7 @@ class Repository end def keep_around_ref_name(sha) - "refs/keep-around/#{sha}" + "refs/#{REF_KEEP_AROUND}/#{sha}" end def repository_event(event, tags = {}) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 09d5ff46618..9533aa7f555 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -10,6 +10,8 @@ class Snippet < ActiveRecord::Base include Spammable include Editable + extend Gitlab::CurrentSettings + cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description cache_markdown_field :content diff --git a/app/models/user.rb b/app/models/user.rb index fbd08bc4d0a..c5b5f09722f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,9 +2,11 @@ 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 Avatarable include Referable include Sortable @@ -303,7 +305,7 @@ class User < ActiveRecord::Base # Returns an ActiveRecord::Relation. def search(query) table = arel_table - pattern = "%#{query}%" + pattern = User.to_pattern(query) order = <<~SQL CASE @@ -601,7 +603,7 @@ class User < ActiveRecord::Base end def require_personal_access_token_creation_for_git_auth? - return false if allow_password_authentication? || ldap_user? + return false if current_application_settings.password_authentication_enabled? || ldap_user? PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none? end @@ -642,11 +644,6 @@ class User < ActiveRecord::Base @personal_projects_count ||= personal_projects.count end - def projects_limit_percent - return 100 if projects_limit.zero? - (personal_projects.count.to_f / projects_limit) * 100 - end - def recent_push(project_ids = nil) # Get push events not earlier than 2 hours ago events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) @@ -664,10 +661,6 @@ class User < ActiveRecord::Base end end - def projects_sorted_by_activity - authorized_projects.sorted_by_activity - end - def several_namespaces? owned_groups.any? || masters_groups.any? end @@ -1048,6 +1041,10 @@ class User < ActiveRecord::Base ensure_rss_token! end + def verified_email?(email) + self.email == email + end + protected # override, from Devise::Validatable diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 5c7c2204374..f2315bb3dbb 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -84,7 +84,7 @@ class WikiPage # The formatted title of this page. def title if @attributes[:title] - self.class.unhyphenize(@attributes[:title]) + CGI.unescape_html(self.class.unhyphenize(@attributes[:title])) else "" end |