diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-07-19 23:33:42 +0200 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-07-19 23:33:42 +0200 |
commit | bf9db45a1dc4d8269fa68dc27dac6667c89ffda8 (patch) | |
tree | 813d6c8151c156d186cdf4e3420f42260d206434 /app/models | |
parent | 0aedeb5637932fa827e42be7441e9c967049dd1d (diff) | |
parent | b9ed9d658ad447a64d58b2040849a7cc0e698287 (diff) | |
download | gitlab-ce-bf9db45a1dc4d8269fa68dc27dac6667c89ffda8.tar.gz |
Merge remote-tracking branch 'origin/master' into ci-predefined-variables
Diffstat (limited to 'app/models')
55 files changed, 1289 insertions, 548 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb index 9c58b956007..6fd18f2ee24 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,5 +1,6 @@ class Ability class << self + # rubocop: disable Metrics/CyclomaticComplexity def allowed(user, subject) return anonymous_abilities(user, subject) if user.nil? return [] unless user.is_a?(User) @@ -19,6 +20,7 @@ class Ability when ProjectMember then project_member_abilities(user, subject) when User then user_abilities when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project) + when Ci::Runner then runner_abilities(user, subject) else [] end.concat(global_abilities(user)) end @@ -155,10 +157,11 @@ class Ability # Push abilities on the users team role rules.push(*project_team_rules(project.team, user)) - if project.owner == user || - (project.group && project.group.has_owner?(user)) || - user.admin? + owner = user.admin? || + project.owner == user || + (project.group && project.group.has_owner?(user)) + if owner rules.push(*project_owner_rules) end @@ -167,6 +170,10 @@ class Ability # Allow to read builds for internal projects rules << :read_build if project.public_builds? + + unless owner || project.team.member?(user) || project_group_member?(project, user) + rules << :request_access + end end if project.archived? @@ -196,7 +203,9 @@ class Ability @public_project_rules ||= project_guest_rules + [ :download_code, :fork_project, - :read_commit_status + :read_commit_status, + :read_pipeline, + :read_container_image ] end @@ -342,8 +351,11 @@ class Ability rules = [] rules << :read_group if can_read_group?(user, group) + owner = user.admin? || group.has_owner?(user) + master = owner || group.has_master?(user) + # Only group masters and group owners can create new projects - if group.has_master?(user) || group.has_owner?(user) || user.admin? + if master rules += [ :create_projects, :admin_milestones @@ -351,7 +363,7 @@ class Ability end # Only group owner and administrators can admin group - if group.has_owner?(user) || user.admin? + if owner rules += [ :admin_group, :admin_namespace, @@ -360,6 +372,10 @@ class Ability ] end + if group.public? || (group.internal? && !user.external?) + rules << :request_access unless group.users.include?(user) + end + rules.flatten end @@ -511,6 +527,18 @@ class Ability rules end + def runner_abilities(user, runner) + if user.is_admin? + [:assign_runner] + elsif runner.is_shared? || runner.locked? + [] + elsif user.ci_authorized_runners.include?(runner) + [:assign_runner] + else + [] + end + end + def user_abilities [:read_user] end @@ -549,5 +577,13 @@ class Ability rules end + + def project_group_member?(project, user) + project.group && + ( + project.group.members.exists?(user_id: user.id) || + project.group.requesters.exists?(user_id: user.id) + ) + end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index d914b0b26eb..c6f77cc055f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -55,6 +55,13 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :repository_storage, + presence: true, + inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + + validates :enabled_git_access_protocol, + inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -134,6 +141,8 @@ class ApplicationSetting < ActiveRecord::Base disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, container_registry_token_expire_delay: 5, + repository_storage: 'default', + user_default_external: false, ) end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 59c7d87f5df..46b17479d6d 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -8,7 +8,7 @@ class AwardEmoji < ActiveRecord::Base belongs_to :user validates :awardable, :user, presence: true - validates :name, presence: true, inclusion: { in: Emoji.emojis_names } + validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names } validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] } participant :user diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d823d67e3ba..cc4809b3abe 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -5,6 +5,7 @@ module Ci belongs_to :erased_by, class_name: 'User' serialize :options + serialize :yaml_variables validates :coverage, numericality: true, allow_blank: true validates_presence_of :ref @@ -13,21 +14,20 @@ module Ci scope :ignore_failures, ->() { where(allow_failure: false) } scope :with_artifacts, ->() { where.not(artifacts_file: nil) } 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) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader acts_as_taggable + before_save :update_artifacts_size, if: :artifacts_file_changed? before_destroy { project } after_create :execute_hooks class << self - def last_month - where('created_at > ?', Date.today - 1.month) - end - def first_pending pending.unstarted.order('created_at ASC').first end @@ -54,7 +54,10 @@ module Ci new_build.stage = build.stage new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request + new_build.yaml_variables = build.yaml_variables + new_build.when = build.when new_build.user = user + new_build.environment = build.environment new_build.save MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build @@ -89,8 +92,31 @@ module Ci end end + def manual? + self.when == 'manual' + end + + def other_actions + pipeline.manual_actions.where.not(id: self) + end + + def playable? + project.builds_enabled? && commands.present? && manual? + end + + def play(current_user = nil) + # Try to queue a current build + if self.queue + self.update(user: current_user) + self + else + # Otherwise we need to create a duplicate + Ci::Build.retry(self, current_user) + end + end + def retryable? - project.builds_enabled? && commands.present? + project.builds_enabled? && commands.present? && complete? end def retried? @@ -300,18 +326,12 @@ module Ci project.valid_runners_token? token end - def can_be_served?(runner) - return false unless has_tags? || runner.run_untagged? - - (tag_list - runner.tag_list).empty? - end - def has_tags? tag_list.any? end def any_runners_online? - project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } + project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) } end def stuck? @@ -335,7 +355,12 @@ module Ci end def artifacts_metadata_entry(path, **options) - Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry + metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( + artifacts_metadata.path, + path, + **options) + + metadata.to_entry end def erase_artifacts! @@ -379,38 +404,30 @@ module Ci self.update(artifacts_expire_at: nil) end - private - - def erase_trace! - self.trace = nil + def when + read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' end - def update_erased!(user = nil) - self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) + def yaml_variables + read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || [] end - def yaml_variables - global_yaml_variables + job_yaml_variables + private + + def update_artifacts_size + self.artifacts_size = if artifacts_file.exists? + artifacts_file.size + else + nil + end end - def global_yaml_variables - if pipeline.config_processor - pipeline.config_processor.global_variables.map do |key, value| - { key: key, value: value, public: true } - end - else - [] - end + def erase_trace! + self.trace = nil end - def job_yaml_variables - if pipeline.config_processor - pipeline.config_processor.job_variables(name).map do |key, value| - { key: key, value: value, public: true } - end - else - [] - end + def update_erased!(user = nil) + self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) end def project_variables @@ -461,5 +478,11 @@ module Ci variables << { key: 'CI_SERVER_URL', value: Gitlab::REVISION, public: true } variables end + + def build_attributes_from_config + return {} unless pipeline.config_processor + + pipeline.config_processor.build_attributes(name) + end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5b264ecffc5..aca8607f4e8 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -6,6 +6,8 @@ module Ci self.table_name = 'ci_commits' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + belongs_to :user + has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id @@ -16,6 +18,7 @@ module Ci # Invalidate object and save if when touched after_touch :update_state + after_save :keep_around_commits def self.truncate_sha(sha) sha[0...8] @@ -37,22 +40,26 @@ module Ci end def git_author_name - commit_data.author_name if commit_data + commit.try(:author_name) end def git_author_email - commit_data.author_email if commit_data + commit.try(:author_email) end def git_commit_message - commit_data.message if commit_data + commit.try(:message) + end + + def git_commit_title + commit.try(:title) end def short_sha Ci::Pipeline.truncate_sha(sha) end - def commit_data + def commit @commit ||= project.commit(sha) rescue nil @@ -62,6 +69,10 @@ module Ci !tag? end + def manual_actions + builds.latest.manual_actions + end + def retryable? builds.latest.any? do |build| build.failed? && build.retryable? @@ -163,13 +174,26 @@ module Ci end def skip_ci? - git_commit_message =~ /(\[ci skip\])/ if git_commit_message + git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message end def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end + # Manually set the notes for a Ci::Pipeline + # There is no ActiveRecord relation between Ci::Pipeline and notes + # as they are related to a commit sha. This method helps importing + # them using the +Gitlab::ImportExport::RelationFactory+ class. + def notes=(notes) + notes.each do |note| + note[:id] = nil + note[:commit_id] = sha + note[:noteable_id] = self['id'] + note.save! + end + end + def notes Note.for_commit_id(sha) end @@ -199,5 +223,12 @@ module Ci self.duration = statuses.latest.duration save end + + def keep_around_commits + return unless project + + project.repository.keep_around(self.sha) + project.repository.keep_around(self.before_sha) + end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index adb65292208..b64ec79ec2b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -4,7 +4,7 @@ module Ci LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = %w[specific shared active paused online] - FORM_EDITABLE = %i[description tag_list active run_untagged] + FORM_EDITABLE = %i[description tag_list active run_untagged locked] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' @@ -26,6 +26,13 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end + scope :assignable_for, ->(project) do + # FIXME: That `to_sql` is needed to workaround a weird Rails bug. + # Without that, placeholders would miss one and couldn't match. + where(locked: false). + where.not("id IN (#{project.runners.select(:id).to_sql})").specific + end + validate :tag_constraints acts_as_taggable @@ -56,7 +63,7 @@ module Ci def assign_to(project, current_user = nil) self.is_shared = false if shared? self.save - project.runner_projects.create!(runner_id: self.id) + project.runner_projects.create(runner_id: self.id) end def display_name @@ -91,6 +98,10 @@ module Ci !shared? end + def can_pick?(build) + assignable_for?(build.project) && accepting_tags?(build) + end + def only_for?(project) projects == [project] end @@ -111,5 +122,13 @@ module Ci 'can not be empty when runner is not allowed to pick untagged jobs') end end + + def assignable_for?(project) + !locked? || projects.exists?(id: project.id) + end + + def accepting_tags?(build) + (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty? + end end end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index b69ae37668c..fcf2b6dc5e2 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,7 +1,7 @@ module Ci class TriggerRequest < ActiveRecord::Base extend Ci::Model - + belongs_to :trigger, class_name: 'Ci::Trigger' belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build' diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index f8d5d4486fd..c9c47ec7419 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -13,6 +13,7 @@ module Ci attr_encrypted :value, mode: :per_attribute_iv_and_salt, + insecure_mode: true, key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' end diff --git a/app/models/commit.rb b/app/models/commit.rb index d69d518fadd..2ef3973c160 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -214,6 +214,13 @@ class Commit @raw.short_id(7) end + def diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: self.parent_id || self.sha, + head_sha: self.sha + ) + end + def pipelines @pipeline ||= project.pipelines.where(sha: sha) end @@ -271,6 +278,32 @@ class Commit merged_merge_request ? 'merge request' : 'commit' end + # Get the URI type of the given path + # + # Used to build URLs to files in the repository in GFM. + # + # path - String path to check + # + # Examples: + # + # uri_type('doc/README.md') # => :blob + # uri_type('doc/logo.png') # => :raw + # uri_type('doc/api') # => :tree + # uri_type('not/found') # => :nil + # + # Returns a symbol + def uri_type(path) + entry = @raw.tree.path(path) + if entry[:type] == :blob + blob = Gitlab::Git::Blob.new(name: entry[:name]) + blob.image? ? :raw : :blob + else + entry[:type] + end + rescue Rugged::TreeError + nil + end + private def repo_changes diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 4066958f67c..630ee9601e0 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -23,7 +23,7 @@ class CommitRange attr_reader :commit_from, :notation, :commit_to attr_reader :ref_from, :ref_to - # Optional Project model + # The Project model attr_accessor :project # The beginning and ending refs can be named or SHAs, and @@ -56,7 +56,7 @@ class CommitRange # Initialize a CommitRange # # range_string - The String commit range. - # project - An optional Project model. + # project - The Project model. # # Raises ArgumentError if `range_string` does not match `PATTERN`. def initialize(range_string, project) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ab13db4b297..535db26240a 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -8,6 +8,8 @@ class CommitStatus < ActiveRecord::Base belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true belongs_to :user + delegate :commit, to: :pipeline + validates :pipeline, presence: true, unless: :importing? validates_presence_of :name @@ -20,6 +22,10 @@ class CommitStatus < ActiveRecord::Base scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status, initial: :pending do + event :queue do + transition skipped: :pending + end + event :run do transition pending: :running end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 539c7c31e30..800a16ab246 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -2,10 +2,11 @@ module Awardable extend ActiveSupport::Concern included do - has_many :award_emoji, as: :awardable, dependent: :destroy + has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy if self < Participable - participant :award_emoji_with_associations + # By default we always load award_emoji user association + participant :award_emoji end end @@ -34,12 +35,9 @@ module Awardable end end - def award_emoji_with_associations - award_emoji.includes(:user) - end - def grouped_awards(with_thumbs: true) - awards = award_emoji_with_associations.group_by(&:name) + # By default we always load award_emoji user association + awards = award_emoji.group_by(&:name) if with_thumbs awards[AwardEmoji::UPVOTE_NAME] ||= [] @@ -67,8 +65,7 @@ module Awardable def create_award_emoji(name, current_user) return unless emoji_awardable? - - award_emoji.create(name: name, user: current_user) + award_emoji.create(name: normalize_name(name), user: current_user) end def remove_award_emoji(name, current_user) @@ -82,4 +79,10 @@ module Awardable create_award_emoji(emoji_name, current_user) end end + + private + + def normalize_name(name) + Gitlab::AwardEmoji.normalize_emoji_name(name) + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0ccd3474b81..acb6f5a2998 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,9 +19,14 @@ module Issuable belongs_to :milestone has_many :notes, as: :noteable, dependent: :destroy do def authors_loaded? - # We check first if we're loaded to not load unnecesarily. + # We check first if we're loaded to not load unnecessarily. loaded? && to_a.all? { |note| note.association(:author).loaded? } end + + def award_emojis_loaded? + # We check first if we're loaded to not load unnecessarily. + loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? } + end end has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links @@ -49,11 +54,10 @@ module Issuable scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } - scope :inc_notes_with_associations, -> { includes(notes: :author) } + scope :inc_notes_with_associations, -> { includes(notes: [ :project, :author, :award_emoji ]) } scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } - delegate :name, :email, to: :author, @@ -112,15 +116,18 @@ module Issuable end def sort(method, excluded_labels: []) - case method.to_s - when 'milestone_due_asc' then order_milestone_due_asc - when 'milestone_due_desc' then order_milestone_due_desc - when 'downvotes_desc' then order_downvotes_desc - when 'upvotes_desc' then order_upvotes_desc - when 'priority' then order_labels_priority(excluded_labels: excluded_labels) - else - order_by(method) - end + sorted = case method.to_s + when 'milestone_due_asc' then order_milestone_due_asc + when 'milestone_due_desc' then order_milestone_due_desc + when 'downvotes_desc' then order_downvotes_desc + when 'upvotes_desc' then order_upvotes_desc + when 'priority' then order_labels_priority(excluded_labels: excluded_labels) + else + order_by(method) + end + + # Break ties with the ID column for pagination + sorted.order(id: :desc) end def order_labels_priority(excluded_labels: []) @@ -257,7 +264,14 @@ module Issuable # already have their authors loaded (possibly because the scope # `inc_notes_with_associations` was used) and skip the inclusion if that's # the case. - notes.authors_loaded? ? notes : notes.includes(:author) + includes = [] + includes << :author unless notes.authors_loaded? + includes << :award_emoji unless notes.award_emojis_loaded? + if includes.any? + notes.includes(includes) + else + notes + end end def updated_tasks diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index f00b5b8497c..ec9e0f1b1d0 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -14,14 +14,14 @@ module Mentionable attr = attr.to_s mentionable_attrs << [attr, options] end + end + included do # Accessor for attributes marked mentionable. - def mentionable_attrs - @mentionable_attrs ||= [] + cattr_accessor :mentionable_attrs, instance_accessor: false do + [] end - end - included do if self < Participable participant -> (user, ext) { all_references(user, extractor: ext) } end @@ -45,7 +45,7 @@ module Mentionable def all_references(current_user = nil, text = nil, extractor: nil) extractor ||= Gitlab::ReferenceExtractor. - new(project, current_user || author) + new(project, current_user) if text extractor.analyze(text, author: author) diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb new file mode 100644 index 00000000000..2785fbb21c9 --- /dev/null +++ b/app/models/concerns/note_on_diff.rb @@ -0,0 +1,52 @@ +module NoteOnDiff + extend ActiveSupport::Concern + + NUMBER_OF_TRUNCATED_DIFF_LINES = 16 + + included do + delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + end + + def diff_note? + true + end + + def diff_file + raise NotImplementedError + end + + def diff_line + raise NotImplementedError + end + + def for_line?(line) + raise NotImplementedError + end + + def diff_attributes + raise NotImplementedError + end + + def can_be_award_emoji? + false + end + + # Returns an array of at most 16 highlighted lines above a diff note + def truncated_diff_lines + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.meta? + prev_lines.clear + else + prev_lines << line + + break if for_line?(line) + + prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + prev_lines + end +end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 9056722f45e..70740c76e43 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -41,9 +41,12 @@ module Participable def participant(attr) participant_attrs << attr end + end - def participant_attrs - @participant_attrs ||= [] + included do + # Accessor for participant attributes. + cattr_accessor :participant_attrs, instance_accessor: false do + [] end end @@ -53,6 +56,16 @@ module Participable # # Returns an Array of User instances. def participants(current_user = nil) + @participants ||= Hash.new do |hash, user| + hash[user] = raw_participants(user) + end + + @participants[current_user] + end + + private + + def raw_participants(current_user = nil) current_user ||= author ext = Gitlab::ReferenceExtractor.new(project, current_user) participants = Set.new diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb index 3ef91caad47..44c6b30f278 100644 --- a/app/models/concerns/statuseable.rb +++ b/app/models/concerns/statuseable.rb @@ -16,10 +16,10 @@ module Statuseable deduce_status = "(CASE WHEN (#{builds})=0 THEN NULL - WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success' - WHEN (#{builds})=(#{pending}) THEN 'pending' - WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored}) THEN 'canceled' WHEN (#{builds})=(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' + WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending' + WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled' WHEN (#{running})+(#{pending})>0 THEN 'running' ELSE 'failed' END)" diff --git a/app/models/deployment.rb b/app/models/deployment.rb index e498ca96e3c..1a7cd60817e 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,6 +11,8 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true + after_save :keep_around_commit + def commit project.commit(sha) end @@ -26,4 +28,12 @@ class Deployment < ActiveRecord::Base def last? self == environment.last_deployment end + + def keep_around_commit + project.repository.keep_around(self.sha) + end + + def manual_actions + deployable.try(:other_actions) + end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb new file mode 100644 index 00000000000..9671955db36 --- /dev/null +++ b/app/models/diff_note.rb @@ -0,0 +1,127 @@ +class DiffNote < Note + include NoteOnDiff + + serialize :original_position, Gitlab::Diff::Position + serialize :position, Gitlab::Diff::Position + + validates :original_position, presence: true + validates :position, presence: true + validates :diff_line, presence: true + validates :line_code, presence: true, line_code: true + validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] } + validate :positions_complete + validate :verify_supported + + before_validation :set_original_position, :update_position, on: :create + before_validation :set_line_code + after_save :keep_around_commits + + class << self + def build_discussion_id(noteable_type, noteable_id, position) + [super(noteable_type, noteable_id), *position.key].join("-") + end + end + + def new_diff_note? + true + end + + def diff_attributes + { position: position.to_json } + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position) + end + + def original_discussion_id + @original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position) + end + + def position=(new_position) + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + super(new_position) + end + + def diff_file + @diff_file ||= self.original_position.diff_file(self.project.repository) + end + + def diff_line + @diff_line ||= diff_file.line_for_position(self.original_position) if diff_file + end + + def for_line?(line) + diff_file.position(line) == self.original_position + end + + def active?(diff_refs = nil) + return false unless supported? + return true if for_commit? + + diff_refs ||= self.noteable.diff_refs + + self.position.diff_refs == diff_refs + end + + private + + def supported? + !self.for_merge_request? || self.noteable.support_new_diff_notes? + end + + def set_original_position + self.original_position = self.position.dup + end + + def set_line_code + self.line_code = self.position.line_code(self.project.repository) + end + + def update_position + return unless supported? + return if for_commit? + + return if active? + + Notes::DiffPositionUpdateService.new( + self.project, + nil, + old_diff_refs: self.position.diff_refs, + new_diff_refs: self.noteable.diff_refs, + paths: self.position.paths + ).execute(self) + end + + def verify_supported + return if supported? + + errors.add(:noteable, "doesn't support new-style diff notes") + end + + def positions_complete + return if self.original_position.complete? && self.position.complete? + + errors.add(:position, "is invalid") + end + + def keep_around_commits + project.repository.keep_around(self.original_position.base_sha) + project.repository.keep_around(self.original_position.start_sha) + project.repository.keep_around(self.original_position.head_sha) + + if self.position != self.original_position + project.repository.keep_around(self.position.base_sha) + project.repository.keep_around(self.position.start_sha) + project.repository.keep_around(self.position.head_sha) + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 716039fb54b..fd736d12359 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -67,7 +67,7 @@ class Event < ActiveRecord::Base elsif issue? || issue_note? Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target) else - ((merge_request? || note?) && target) || milestone? + ((merge_request? || note?) && target.present?) || milestone? end end @@ -136,7 +136,7 @@ class Event < ActiveRecord::Base end def note? - target_type == "Note" + target.is_a?(Note) end def issue? @@ -315,7 +315,7 @@ class Event < ActiveRecord::Base def body? if push? - push_with_commits? + push_with_commits? || rm_ref? elsif note? true else diff --git a/app/models/group.rb b/app/models/group.rb index e66e04371b2..37631b99701 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -6,15 +6,16 @@ class Group < Namespace include AccessRequestable include Referable - has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' + has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' alias_method :members, :group_members - has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members - + has_many :users, through: :group_members has_many :owners, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :user + has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' + has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source @@ -89,7 +90,7 @@ class Group < Namespace end def avatar_url(size = nil) - if avatar.present? + if self[:avatar].present? [gitlab_config.url, avatar.url].join end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 3c5859194b4..60abd47409e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -19,6 +19,8 @@ class Issue < ActiveRecord::Base belongs_to :project belongs_to :moved_to, class_name: 'Issue' + has_many :events, as: :target, dependent: :destroy + validates :project, presence: true scope :cared, ->(user) { where(assignee_id: user) } diff --git a/app/models/key.rb b/app/models/key.rb index 0532e84f47d..b9bc38a0436 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -9,7 +9,7 @@ class Key < ActiveRecord::Base before_validation :strip_white_space, :generate_fingerprint validates :title, presence: true, length: { within: 0..255 } - validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true + validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } validates :key, format: { without: /\n|\r/, message: 'should be a single line' } validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } diff --git a/app/models/label.rb b/app/models/label.rb index 49c352cc239..35e678001dc 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -20,10 +20,10 @@ class Label < ActiveRecord::Base validates :color, color: true, allow_blank: false validates :project, presence: true, unless: Proc.new { |service| service.template? } - # Don't allow '?', '&', and ',' for label titles + # Don't allow ',' for label titles validates :title, presence: true, - format: { with: /\A[^&\?,]+\z/ }, + format: { with: /\A[^,]+\z/ }, uniqueness: { scope: :project_id } before_save :nullify_priority @@ -52,14 +52,17 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern + # NOTE: The id pattern only matches when all characters on the expression + # are digits, so it will match ~2 but not ~2fa because that's probably a + # label name and we want it to be matched as such. @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: - (?<label_id>\d+) | # Integer-based label ID, or + (?<label_id>\d+(?!\S\w)\b) | # Integer-based label ID, or (?<label_name> - [A-Za-z0-9_-]+ | # String-based single-word label title, or - "[^&\?,]+" # String-based multi-word label surrounded in quotes + [A-Za-z0-9_\-\?\.&]+ | # String-based single-word label title, or + ".+?" # String-based multi-word label surrounded in quotes ) ) }x @@ -114,7 +117,7 @@ class Label < ActiveRecord::Base end def title=(value) - write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? + write_attribute(:title, sanitize_title(value)) if value.present? end private @@ -132,4 +135,8 @@ class Label < ActiveRecord::Base def nullify_priority self.priority = nil if priority.blank? end + + def sanitize_title(value) + CGI.unescapeHTML(Sanitize.clean(value.to_s)) + end end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 95fd510eb3a..04a651d50ab 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -1,4 +1,6 @@ class LegacyDiffNote < Note + include NoteOnDiff + serialize :st_diff validates :line_code, presence: true, line_code: true @@ -11,77 +13,36 @@ class LegacyDiffNote < Note end end - def diff_note? + def legacy_diff_note? true end - def legacy_diff_note? - true + def diff_attributes + { line_code: line_code } end def discussion_id - @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) end def diff_file_hash line_code.split('_')[0] if line_code end - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code - end - def diff @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) end - def diff_file_path - diff.new_path.presence || diff.old_path - end - - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + def diff_file + @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff end def diff_line - @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + @diff_line ||= diff_file.line_for_line_code(self.line_code) if diff_file end - def diff_line_text - diff_line.try(:text) - end - - def diff_line_type - diff_line.try(:type) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines - end - end - - prev_lines + def for_line?(line) + !line.meta? && diff_file.line_code(line) == self.line_code end # Check if this note is part of an "active" discussion @@ -94,7 +55,7 @@ class LegacyDiffNote < Note def active? return @active if defined?(@active) return true if for_commit? - return true unless self.diff + return true unless diff_line return false unless noteable noteable_diff = find_noteable_diff @@ -102,7 +63,7 @@ class LegacyDiffNote < Note if noteable_diff parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line.text } else @active = false end @@ -110,10 +71,6 @@ class LegacyDiffNote < Note @active end - def award_emoji_supported? - false - end - private def find_diff @@ -149,10 +106,6 @@ class LegacyDiffNote < Note self.class.where(attributes).last.try(:diff) end - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) - end - # Find the diff on noteable that matches our own def find_noteable_diff diffs = noteable.diffs(Commit.max_diff_options) diff --git a/app/models/member.rb b/app/models/member.rb index 4ee3f1bb5c2..44db3d977fa 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -30,8 +30,7 @@ class Member < ActiveRecord::Base scope :invite, -> { where.not(invite_token: nil) } scope :non_invite, -> { where(invite_token: nil) } scope :request, -> { where.not(requested_at: nil) } - scope :non_request, -> { where(requested_at: nil) } - scope :non_pending, -> { non_request.non_invite } + scope :has_access, -> { where('access_level > 0') } scope :guests, -> { where(access_level: GUEST) } scope :reporters, -> { where(access_level: REPORTER) } @@ -48,7 +47,6 @@ class Member < ActiveRecord::Base after_create :post_create_hook, unless: [:pending?, :importing?] after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? - after_destroy :post_decline_request, if: :request? delegate :name, :username, :email, to: :user, prefix: true @@ -188,7 +186,7 @@ class Member < ActiveRecord::Base end def send_request - # override in subclass + notification_service.new_access_request(self) end def post_create_hook @@ -215,10 +213,6 @@ class Member < ActiveRecord::Base post_create_hook end - def post_decline_request - # override in subclass - end - def system_hook_service SystemHooksService.new end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 363db877968..2f13d339c89 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -33,12 +33,6 @@ class GroupMember < Member super end - def send_request - notification_service.new_group_access_request(self) - - super - end - def post_create_hook notification_service.new_group_member(self) @@ -64,10 +58,4 @@ class GroupMember < Member super end - - def post_decline_request - notification_service.decline_group_access_request(self) - - super - end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 250ee04fd1d..f39afc61ce9 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -15,7 +15,6 @@ class ProjectMember < Member before_destroy :delete_member_todos class << self - # Add users to project teams with passed access option # # access can be an integer representing a access code @@ -111,12 +110,6 @@ class ProjectMember < Member super end - def send_request - notification_service.new_project_access_request(self) - - super - end - def post_create_hook unless owner? event_service.join_project(self.project, self.user) @@ -152,12 +145,6 @@ class ProjectMember < Member super end - def post_decline_request - notification_service.decline_project_access_request(self) - - super - end - def event_service EventCreateService.new end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 36bc98bdb1e..471e32f3b60 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -12,12 +12,14 @@ class MergeRequest < ActiveRecord::Base has_one :merge_request_diff, dependent: :destroy + has_many :events, as: :target, dependent: :destroy + serialize :merge_params, Hash - after_create :create_merge_request_diff, unless: :importing + after_create :create_merge_request_diff, unless: :importing? after_update :update_merge_request_diff - delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil + delegate :commits, :real_size, to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -27,10 +29,6 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare - # Temporary fields to store target_sha, and base_sha to - # compare when importing pull requests from GitHub - attr_accessor :base_target_sha, :head_source_sha - state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -87,12 +85,7 @@ class MergeRequest < ActiveRecord::Base state :cannot_be_merged around_transition do |merge_request, transition, block| - merge_request.record_timestamps = false - begin - block.call - ensure - merge_request.record_timestamps = true - end + Gitlab::Timeless.timeless(merge_request, &block) end end @@ -115,6 +108,8 @@ class MergeRequest < ActiveRecord::Base scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } + after_save :keep_around_commit + def self.reference_prefix '!' end @@ -165,28 +160,99 @@ class MergeRequest < ActiveRecord::Base reference end - def last_commit - merge_request_diff ? merge_request_diff.last_commit : compare_commits.last - end - def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end + def diffs(*args) + merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) + end + def diff_size merge_request_diff.size end def diff_base_commit - if merge_request_diff + if persisted? merge_request_diff.base_commit - elsif source_sha - self.target_project.merge_base_commit(self.source_sha, self.target_branch) + elsif diff_start_commit && diff_head_commit + self.target_project.merge_base_commit(diff_start_sha, diff_head_sha) end end - def last_commit_short_sha - last_commit.short_id + # MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha, + # but we need to get a commit for the "View file @ ..." link by deleted files, + # so we find the likely one if we can't get the actual one. + # This will not be the actual base commit if the target branch was merged into + # the source branch after the merge request was created, but it is good enough + # for the specific purpose of linking to a commit. + # It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the + # true base commit, so we can't simply have `#diff_base_commit` fall back on + # this method. + def likely_diff_base_commit + first_commit.parent || first_commit + end + + def diff_start_commit + if persisted? + merge_request_diff.start_commit + else + target_branch_head + end + end + + def diff_head_commit + if persisted? + merge_request_diff.head_commit + else + source_branch_head + end + end + + def diff_start_sha + diff_start_commit.try(:sha) + end + + def diff_base_sha + diff_base_commit.try(:sha) + end + + def diff_head_sha + diff_head_commit.try(:sha) + end + + # When importing a pull request from GitHub, the old and new branches may no + # longer actually exist by those names, but we need to recreate the merge + # request diff with the right source and target shas. + # We use these attributes to force these to the intended values. + attr_writer :target_branch_sha, :source_branch_sha + + def source_branch_head + source_branch_ref = @source_branch_sha || source_branch + source_project.repository.commit(source_branch) if source_branch_ref + end + + def target_branch_head + target_branch_ref = @target_branch_sha || target_branch + target_project.repository.commit(target_branch) if target_branch_ref + end + + def target_branch_sha + target_branch_head.try(:sha) + end + + def source_branch_sha + source_branch_head.try(:sha) + end + + def diff_refs + return unless diff_start_commit || diff_base_commit + + Gitlab::Diff::DiffRefs.new( + base_sha: diff_base_sha, + start_sha: diff_start_sha, + head_sha: diff_head_sha + ) end def validate_branches @@ -223,21 +289,30 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? - reload_code + reload_diff end end - def reload_code - if merge_request_diff && open? - merge_request_diff.reload_content - end + def reload_diff + return unless merge_request_diff && open? + + old_diff_refs = self.diff_refs + + merge_request_diff.reload_content + + new_diff_refs = self.diff_refs + + update_diff_notes_positions( + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) end def check_if_can_be_merged return unless unchecked? can_be_merged = - !broken? && project.repository.can_be_merged?(source_sha, target_branch) + !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) if can_be_merged mark_as_mergeable @@ -247,11 +322,11 @@ class MergeRequest < ActiveRecord::Base end def merge_event - self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last + @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last end def closed_event - self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last + @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze @@ -264,19 +339,19 @@ class MergeRequest < ActiveRecord::Base self.title.sub(WIP_REGEX, "") end - def mergeable? - return false unless mergeable_state? + def mergeable?(skip_ci_check: false) + return false unless mergeable_state?(skip_ci_check: skip_ci_check) check_if_can_be_merged can_be_merged? end - def mergeable_state? + def mergeable_state?(skip_ci_check: false) return false unless open? return false if work_in_progress? return false if broken? - return false unless mergeable_ci_state? + return false unless skip_ci_check || mergeable_ci_state? true end @@ -289,7 +364,7 @@ class MergeRequest < ActiveRecord::Base !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && Ability.abilities.allowed?(current_user, :push_code, source_project) && - last_commit == source_project.commit(source_branch) + diff_head_commit == source_branch_head end def should_remove_source_branch? @@ -319,13 +394,6 @@ class MergeRequest < ActiveRecord::Base ) end - # Returns the commit as a series of email patches. - # - # see "git format-patch" - def to_patch - target_project.repository.format_patch(diff_base_commit.sha, source_sha) - end - def hook_attrs attrs = { source: source_project.try(:hook_attrs), @@ -334,8 +402,8 @@ class MergeRequest < ActiveRecord::Base work_in_progress: work_in_progress? } - if last_commit - attrs.merge!(last_commit: last_commit.hook_attrs) + if diff_head_commit + attrs.merge!(last_commit: diff_head_commit.hook_attrs) end attributes.merge!(attrs) @@ -484,7 +552,13 @@ class MergeRequest < ActiveRecord::Base end def can_be_merged_by?(user) - ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) + access = ::Gitlab::UserAccess.new(user, project: project) + access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch) + end + + def can_be_merged_via_command_line_by?(user) + access = ::Gitlab::UserAccess.new(user, project: project) + access.can_push_to_branch?(target_branch) end def mergeable_ci_state? @@ -513,22 +587,6 @@ class MergeRequest < ActiveRecord::Base end end - def target_sha - return @base_target_sha if defined?(@base_target_sha) - - target_project.repository.commit(target_branch).try(:sha) - end - - def source_sha - return @head_source_sha if defined?(@head_source_sha) - - last_commit.try(:sha) || source_tip.try(:sha) - end - - def source_tip - source_branch && source_project.repository.commit(source_branch) - end - def fetch_ref target_project.repository.fetch_ref( source_project.repository.path_to_repo, @@ -541,12 +599,12 @@ class MergeRequest < ActiveRecord::Base "refs/merge-requests/#{iid}/head" end - def ref_is_fetched? - File.exist?(File.join(project.repository.path_to_repo, ref_path)) + def ref_fetched? + project.repository.ref_exists?(ref_path) end def ensure_ref_fetched - fetch_ref unless ref_is_fetched? + fetch_ref unless ref_fetched? end def in_locked_state @@ -561,10 +619,10 @@ class MergeRequest < ActiveRecord::Base def diverged_commits_count cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") - if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha + if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha cache = { - source_sha: source_sha, - target_sha: target_sha, + source_sha: source_branch_sha, + target_sha: target_branch_sha, diverged_commits_count: compute_diverged_commits_count } Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache) @@ -574,9 +632,9 @@ class MergeRequest < ActiveRecord::Base end def compute_diverged_commits_count - return 0 unless source_sha && target_sha + return 0 unless source_branch_sha && target_branch_sha - Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size + Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size end private :compute_diverged_commits_count @@ -585,13 +643,7 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project - end - - def diff_refs - return nil unless diff_base_commit - - [diff_base_commit, last_commit] + @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project end def merge_commit @@ -605,4 +657,38 @@ class MergeRequest < ActiveRecord::Base def can_be_cherry_picked? merge_commit end + + def support_new_diff_notes? + diff_refs && diff_refs.complete? + end + + def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) + return unless support_new_diff_notes? + return if new_diff_refs == old_diff_refs + + active_diff_notes = self.notes.diff_notes.select do |note| + note.new_diff_note? && note.active?(old_diff_refs) + end + + return if active_diff_notes.empty? + + paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq + + service = Notes::DiffPositionUpdateService.new( + self.project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs, + paths: paths + ) + + active_diff_notes.each do |note| + service.execute(note) + Gitlab::Timeless.timeless(note, &:save) + end + end + + def keep_around_commit + project.repository.keep_around(self.merge_commit_sha) + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index aca377cc600..3f520c8f3ff 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,13 +1,14 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable include Importable + include EncodingHelper # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 belongs_to :merge_request - delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -24,6 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_diffs after_create :reload_content, unless: :importing? + after_save :keep_around_commits, unless: :importing? def reload_content reload_commits @@ -38,14 +40,15 @@ class MergeRequestDiff < ActiveRecord::Base if options[:ignore_whitespace_change] @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( - self.repository.raw_repository, - self.base, - self.head, + repository.raw_repository, + self.start_commit_sha || self.target_branch_sha, + self.head_commit_sha || self.source_branch_sha, ) compare.diffs(options) end else - @diffs ||= load_diffs(st_diffs, options) + @diffs ||= {} + @diffs[options] ||= load_diffs(st_diffs, options) end end @@ -62,37 +65,39 @@ class MergeRequestDiff < ActiveRecord::Base end def base_commit - return nil unless self.base_commit_sha + return unless self.base_commit_sha - merge_request.target_project.commit(self.base_commit_sha) + project.commit(self.base_commit_sha) end - def last_commit_short_sha - @last_commit_short_sha ||= last_commit.short_id - end + def start_commit + return unless self.start_commit_sha - def dump_commits(commits) - commits.map(&:to_hash) + project.commit(self.start_commit_sha) end - def load_commits(array) - array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } - end + def head_commit + return last_commit unless self.head_commit_sha - def dump_diffs(diffs) - if diffs.respond_to?(:map) - diffs.map(&:to_hash) - end + project.commit(self.head_commit_sha) end - def load_diffs(raw, options) - if raw.respond_to?(:each) - Gitlab::Git::DiffCollection.new(raw, options) - else - Gitlab::Git::DiffCollection.new([]) - end + def compare + @compare ||= + begin + # Update ref for merge request + merge_request.fetch_ref + + Gitlab::Git::Compare.new( + repository.raw_repository, + self.target_branch_sha, + self.source_branch_sha + ) + end end + private + # Collect array of Git::Commit objects # between target and source branches def unmerged_commits @@ -105,89 +110,142 @@ class MergeRequestDiff < ActiveRecord::Base commits end + def dump_commits(commits) + commits.map(&:to_hash) + end + + def load_commits(array) + array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } + end + # Reload all commits related to current merge request from repo # and save it as array of hashes in st_commits db field def reload_commits + new_attributes = {} + commit_objects = unmerged_commits if commit_objects.present? - self.st_commits = dump_commits(commit_objects) + new_attributes[:st_commits] = dump_commits(commit_objects) end - save + update_columns_serialized(new_attributes) + end + + # Collect array of Git::Diff objects + # between target and source branches + def unmerged_diffs + compare.diffs(Commit.max_diff_options) + end + + def dump_diffs(diffs) + if diffs.respond_to?(:map) + diffs.map(&:to_hash) + end + end + + def load_diffs(raw, options) + if raw.respond_to?(:each) + if paths = options[:paths] + raw = raw.select do |diff| + paths.include?(diff[:old_path]) || paths.include?(diff[:new_path]) + end + end + + Gitlab::Git::DiffCollection.new(raw, options) + else + Gitlab::Git::DiffCollection.new([]) + end end # Reload diffs between branches related to current merge request from repo # and save it as array of hashes in st_diffs db field def reload_diffs + new_attributes = {} new_diffs = [] if commits.size.zero? - self.state = :empty + new_attributes[:state] = :empty else diff_collection = unmerged_diffs if diff_collection.overflow? # Set our state to 'overflow' to make the #empty? and #collected? # methods (generated by StateMachine) return false. - self.state = :overflow + new_attributes[:state] = :overflow end - self.real_size = diff_collection.real_size + new_attributes[:real_size] = diff_collection.real_size if diff_collection.any? new_diffs = dump_diffs(diff_collection) - self.state = :collected + new_attributes[:state] = :collected end end - self.st_diffs = new_diffs + new_attributes[:st_diffs] = new_diffs - self.base_commit_sha = self.repository.merge_base(self.head, self.base) + new_attributes[:start_commit_sha] = self.target_branch_sha + new_attributes[:head_commit_sha] = self.source_branch_sha + new_attributes[:base_commit_sha] = branch_base_sha - self.save + update_columns_serialized(new_attributes) + + keep_around_commits end - # Collect array of Git::Diff objects - # between target and source branches - def unmerged_diffs - compare.diffs(Commit.max_diff_options) + def project + merge_request.target_project end def repository - merge_request.target_project.repository + project.repository end - def source_sha - return head_source_sha if head_source_sha.present? + def branch_base_commit + return unless self.source_branch_sha && self.target_branch_sha - source_commit = merge_request.source_project.commit(source_branch) - source_commit.try(:sha) + project.merge_base_commit(self.source_branch_sha, self.target_branch_sha) end - def target_sha - merge_request.target_sha + def branch_base_sha + branch_base_commit.try(:sha) end - def base - self.target_sha || self.target_branch - end - - def head - self.source_sha + def utf8_st_diffs + st_diffs.map do |diff| + diff.each do |k, v| + diff[k] = encode_utf8(v) if v.respond_to?(:encoding) + end + end end - def compare - @compare ||= - begin - # Update ref for merge request - merge_request.fetch_ref - - Gitlab::Git::Compare.new( - self.repository.raw_repository, - self.base, - self.head - ) - end + # + # #save or #update_attributes providing changes on serialized attributes do a lot of + # serialization and deserialization calls resulting in bad performance. + # Using #update_columns solves the problem with just one YAML.dump per serialized attribute that we provide. + # As a tradeoff we need to reload the current instance to properly manage time objects on those serialized + # attributes. So to keep the same behaviour as the attribute assignment we reload the instance. + # The difference is in the usage of + # #write_attribute= (#update_attributes) and #raw_write_attribute= (#update_columns) + # + # Ex: + # + # new_attributes[:st_commits].first.slice(:committed_date) + # => {:committed_date=>2014-02-27 11:01:38 +0200} + # YAML.load(YAML.dump(new_attributes[:st_commits].first.slice(:committed_date))) + # => {:committed_date=>2014-02-27 10:01:38 +0100} + # + def update_columns_serialized(new_attributes) + return unless new_attributes.any? + + update_columns(new_attributes.merge(updated_at: current_time_from_proper_timezone)) + reload + end + + def keep_around_commits + repository.keep_around(target_branch_sha) + repository.keep_around(source_branch_sha) + repository.keep_around(branch_base_sha) end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e0c8454a998..2bd7f198030 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -17,6 +17,7 @@ class Milestone < ActiveRecord::Base has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee + has_many :events, as: :target, dependent: :destroy scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index da19462f265..8b52cc824cd 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -21,8 +21,10 @@ class Namespace < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true - after_create :ensure_dir_exist after_update :move_dir, if: :path_changed? + + # Save the storage paths before the projects are destroyed to use them on after destroy + before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths } after_destroy :rm_dir scope :root, -> { where('type IS NULL') } @@ -87,51 +89,35 @@ class Namespace < ActiveRecord::Base owner_name end - def ensure_dir_exist - gitlab_shell.add_namespace(path) - end - - def rm_dir - # Move namespace directory into trash. - # We will remove it later async - new_path = "#{path}+#{id}+deleted" - - if gitlab_shell.mv_namespace(path, new_path) - message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" - Gitlab::AppLogger.info message - - # Remove namespace directroy async with delay so - # GitLab has time to remove all projects first - GitlabShellWorker.perform_in(5.minutes, :rm_namespace, new_path) - end - end - def move_dir - # Ensure old directory exists before moving it - gitlab_shell.add_namespace(path_was) - if any_project_has_container_registry_tags? raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') end - if gitlab_shell.mv_namespace(path_was, path) - Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) - - # If repositories moved successfully we need to - # send update instructions to users. - # However we cannot allow rollback since we moved namespace dir - # So we basically we mute exceptions in next actions - begin - send_update_instructions - rescue - # Returning false does not rollback after_* transaction but gives - # us information about failing some of tasks - false + # Move the namespace directory in all storages paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') end - else - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise Exception.new('namespace directory cannot be moved') + end + + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + + # If repositories moved successfully we need to + # send update instructions to users. + # However we cannot allow rollback since we moved namespace dir + # So we basically we mute exceptions in next actions + begin + send_update_instructions + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false end end @@ -152,4 +138,33 @@ class Namespace < ActiveRecord::Base def find_fork_of(project) projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) end + + private + + def repository_storage_paths + # We need to get the storage paths for all the projects, even the ones that are + # pending delete. Unscoping also get rids of the default order, which causes + # problems with SELECT DISTINCT. + Project.unscoped do + projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) + end + end + + def rm_dir + # Remove the namespace directory in all storages paths used by member projects + @old_repository_storage_paths.each do |repository_storage_path| + # Move namespace directory into trash. + # We will remove it later async + new_path = "#{path}+#{id}+deleted" + + if gitlab_shell.mv_namespace(repository_storage_path, path, new_path) + message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + Gitlab::AppLogger.info message + + # Remove namespace directroy async with delay so + # GitLab has time to remove all projects first + GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) + end + end + end end diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index a2aee2f925b..345041a6ad1 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -54,7 +54,7 @@ module Network @map = {} @reserved = {} - @commits.each_with_index do |c,i| + @commits.each_with_index do |c, i| c.time = i days[i] = c.committed_date @map[c.id] = c @@ -116,7 +116,7 @@ module Network end def commits_sort_by_ref - @commits.sort do |a,b| + @commits.sort do |a, b| if include_ref?(a) -1 elsif include_ref?(b) diff --git a/app/models/note.rb b/app/models/note.rb index 8d164647550..0ce10c77de9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -6,6 +6,14 @@ class Note < ActiveRecord::Base include Awardable include Importable + # Attribute containing rendered and redacted Markdown as generated by + # Banzai::ObjectRenderer. + attr_accessor :note_html + + # An Array containing the number of visible references as generated by + # Banzai::ObjectRenderer + attr_accessor :user_visible_reference_count + default_value_for :system, false attr_mentionable :note, pipeline: :note @@ -17,6 +25,7 @@ class Note < ActiveRecord::Base belongs_to :updated_by, class_name: "User" has_many :todos, dependent: :destroy + has_many :events, as: :target, dependent: :destroy delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true @@ -49,16 +58,19 @@ class Note < ActiveRecord::Base scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } + scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) } - scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) } scope :with_associations, -> do + # FYI noteable cannot be loaded for LegacyDiffNote for commits includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end before_validation :clear_blank_line_code! + after_save :keep_around_commit class << self def model_name @@ -74,7 +86,7 @@ class Note < ActiveRecord::Base end def grouped_diff_notes - legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) + diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. @@ -107,6 +119,10 @@ class Note < ActiveRecord::Base false end + def new_diff_note? + false + end + def active? true end @@ -181,11 +197,19 @@ class Note < ActiveRecord::Base end def cross_reference_not_visible_for?(user) - cross_reference? && referenced_mentionables(user).empty? + cross_reference? && !has_referenced_mentionables?(user) + end + + def has_referenced_mentionables?(user) + if user_visible_reference_count.present? + user_visible_reference_count > 0 + else + referenced_mentionables(user).any? + end end def award_emoji? - award_emoji_supported? && contains_emoji_only? + can_be_award_emoji? && contains_emoji_only? end def emoji_awardable? @@ -196,7 +220,7 @@ class Note < ActiveRecord::Base self.line_code = nil if self.line_code.blank? end - def award_emoji_supported? + def can_be_award_emoji? noteable.is_a?(Awardable) end @@ -205,7 +229,12 @@ class Note < ActiveRecord::Base end def award_emoji_name - original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - Gitlab::AwardEmoji.normalize_emoji_name(original_name) + note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] + end + + private + + def keep_around_commit + project.repository.keep_around(self.commit_id) end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index d41fc7073c6..121b598b8f3 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -5,6 +5,7 @@ class NotificationSetting < ActiveRecord::Base belongs_to :user belongs_to :source, polymorphic: true + belongs_to :project, foreign_key: 'source_id' validates :user, presence: true validates :level, presence: true @@ -13,7 +14,13 @@ class NotificationSetting < ActiveRecord::Base allow_nil: true } scope :for_groups, -> { where(source_type: 'Namespace') } - scope :for_projects, -> { where(source_type: 'Project') } + + # Exclude projects not included by the Project model's default scope (those that are + # pending delete). + # + scope :for_projects, -> do + includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil }) + end EMAIL_EVENTS = [ :new_note, diff --git a/app/models/project.rb b/app/models/project.rb index ca3bc04e2dd..a805f5d97bc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -24,8 +24,12 @@ class Project < ActiveRecord::Base default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :container_registry_enabled, gitlab_config_features.container_registry + default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } + after_create :ensure_dir_exist + after_save :ensure_dir_exist, if: :namespace_id_changed? + # set last_activity_at to the same as created_at after_create :set_last_activity_at def set_last_activity_at @@ -81,6 +85,7 @@ class Project < ActiveRecord::Base has_one :jira_service, dependent: :destroy has_one :redmine_service, dependent: :destroy has_one :custom_issue_tracker_service, dependent: :destroy + has_one :bugzilla_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy @@ -103,9 +108,13 @@ class Project < ActiveRecord::Base has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy - has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' + + has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember' alias_method :members, :project_members - has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members + has_many :users, through: :project_members + + has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember' + has_many :deploy_keys_projects, dependent: :destroy has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects, dependent: :destroy @@ -153,9 +162,7 @@ class Project < ActiveRecord::Base validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id - validates :import_url, - url: { protocols: %w(ssh git http https) }, - if: :external_import? + validates :import_url, addressable_url: true, if: :external_import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, @@ -163,6 +170,10 @@ class Project < ActiveRecord::Base validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validate :visibility_level_allowed_by_group validate :visibility_level_allowed_as_fork + validate :check_wiki_path_conflict + validates :repository_storage, + presence: true, + inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } add_authentication_token_field :runners_token before_save :ensure_runners_token @@ -374,6 +385,10 @@ class Project < ActiveRecord::Base end end + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage] + end + def team @team ||= ProjectTeam.new(self) end @@ -410,8 +425,8 @@ class Project < ActiveRecord::Base container_registry_repository.tags.any? end - def commit(id = 'HEAD') - repository.commit(id) + def commit(ref = 'HEAD') + repository.commit(ref) end def merge_base_commit(first_commit_id, second_commit_id) @@ -446,9 +461,11 @@ class Project < ActiveRecord::Base end def import_url=(value) + return super(value) unless Gitlab::UrlSanitizer.valid?(value) + import_url = Gitlab::UrlSanitizer.new(value) - create_or_update_import_data(credentials: import_url.credentials) super(import_url.sanitized_url) + create_or_update_import_data(credentials: import_url.credentials) end def import_url @@ -460,7 +477,13 @@ class Project < ActiveRecord::Base end end + def valid_import_url? + valid? || errors.messages[:import_url].nil? + end + def create_or_update_import_data(data: nil, credentials: nil) + return unless import_url.present? && valid_import_url? + project_import_data = import_data || build_import_data if data project_import_data.data ||= {} @@ -539,6 +562,16 @@ class Project < ActiveRecord::Base self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.") end + def check_wiki_path_conflict + return if path.blank? + + path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki" + + if Project.where(namespace_id: namespace_id, path: path_to_check).exists? + errors.add(:name, 'has already been taken') + end + end + def to_param path end @@ -674,7 +707,7 @@ class Project < ActiveRecord::Base end def avatar_url - if avatar.present? + if self[:avatar].present? [gitlab_config.url, avatar.url].join elsif avatar_in_git Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self) @@ -775,18 +808,12 @@ class Project < ActiveRecord::Base @repo_exists = false end + # Branches that are not _exactly_ matched by a protected branch. def open_branches - # We're using a Set here as checking values in a large Set is faster than - # checking values in a large Array. - protected_set = Set.new(protected_branch_names) - - repository.branches.reject do |branch| - protected_set.include?(branch.name) - end - end - - def protected_branch_names - @protected_branch_names ||= protected_branches.pluck(:name) + exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name) + branch_names = repository.branches.map(&:name) + non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names)) + repository.branches.reject { |branch| non_open_branch_names.include? branch.name } end def root_ref?(branch) @@ -803,11 +830,16 @@ class Project < ActiveRecord::Base # Check if current branch name is marked as protected in the system def protected_branch?(branch_name) - protected_branch_names.include?(branch_name) + @protected_branches ||= self.protected_branches.to_a + ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end def developers_can_push_to_protected_branch?(branch_name) - protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push } + protected_branches.matching(branch_name).any?(&:developers_can_push) + end + + def developers_can_merge_to_protected_branch?(branch_name) + protected_branches.matching(branch_name).any?(&:developers_can_merge) end def forked? @@ -830,12 +862,12 @@ class Project < ActiveRecord::Base raise Exception.new('Project cannot be renamed, because tags are present in its container registry') end - if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) + if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) # 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("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") + gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) reset_events_cache @@ -976,7 +1008,7 @@ class Project < ActiveRecord::Base def create_repository # Forked import is handled asynchronously unless forked? - if gitlab_shell.add_repository(path_with_namespace) + if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) repository.after_create true else @@ -1010,8 +1042,8 @@ class Project < ActiveRecord::Base pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_pipeline(sha, ref) - pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref) + def ensure_pipeline(sha, ref, current_user = nil) + pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user) end def enable_ci @@ -1128,4 +1160,8 @@ class Project < ActiveRecord::Base _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) status.zero? end + + def ensure_dir_exist + gitlab_shell.add_namespace(repository_storage_path, namespace.path) + end end diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index ca8a9b4217b..331123a5a5b 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -7,6 +7,7 @@ class ProjectImportData < ActiveRecord::Base marshal: true, encode: true, mode: :per_attribute_iv_and_salt, + insecure_mode: true, algorithm: 'aes-256-cbc' serialize :data, JSON diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb new file mode 100644 index 00000000000..81af55aa29a --- /dev/null +++ b/app/models/project_services/bugzilla_service.rb @@ -0,0 +1,23 @@ +class BugzillaService < IssueTrackerService + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'Bugzilla' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Bugzilla issue tracker' + end + end + + def to_param + 'bugzilla' + end +end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 54da4d74fc5..5e166471077 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -42,6 +42,19 @@ class BuildsEmailService < Service end end + def can_test? + project.builds.count > 0 + end + + def disabled_title + "Please setup a build on your repository." + end + + def test_data(project = nil, user = nil) + build = project.builds.last + Gitlab::BuildDataBuilder.build(build) + end + def fields [ { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' }, @@ -50,6 +63,20 @@ class BuildsEmailService < Service ] end + def test(data) + begin + # bypass build status verification when testing + data[:build_status] = "failed" + data[:build_allow_failure] = false + + result = execute(data) + rescue StandardError => error + return { success: false, result: error } + end + + { success: true, result: result } + end + def should_build_be_notified?(data) case data[:build_status] when 'success' diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 6b2b1daa724..63a5ed14484 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,5 +1,4 @@ class CustomIssueTrackerService < IssueTrackerService - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title @@ -31,8 +30,4 @@ class CustomIssueTrackerService < IssueTrackerService { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' } ] end - - def initialize_properties - self.properties = {} if properties.nil? - end end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 966dbc41d73..5e4dd101c53 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,5 +1,4 @@ class DroneCiService < CiService - prop_accessor :drone_url, :token, :enable_ssl_verification validates :drone_url, presence: true, url: true, if: :activated? diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 0ff4f4c8dd2..23e5b16221b 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -106,7 +106,7 @@ class HipchatService < Service else message << "pushed to #{ref_type} <a href=\""\ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> " - message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> " + message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/, '')}</a> " message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" push[:commits].take(MAX_COMMITS).each do |commit| diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 58cb720c3c1..ce7d1c5d5b1 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -112,15 +112,7 @@ class IrkerService < Service # Authorize both irc://domain.com/#chan and irc://domain.com/chan if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil? - # Do not authorize irc://domain.com/ - if uri.fragment.nil? && uri.path.length > 1 - uri.to_s - else - # Authorize irc://domain.com/smthg#chan - # The irker daemon will deal with it by concatenating smthg and - # chan, thus sending messages on #smthgchan - uri.to_s - end + uri.to_s end end end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 87ecb3b8b86..d1df6d0292f 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,5 +1,4 @@ class IssueTrackerService < Service - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? default_value_for :category, 'issue_tracker' diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index beda89d3963..97bcbacf2b9 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -124,7 +124,7 @@ class JiraService < IssueTrackerService def build_api_url_from_project_url server = URI(project_url) - default_ports = [["http",80],["https",443]].include?([server.scheme,server.port]) + default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port]) server_url = "#{server.scheme}://#{server.host}" server_url.concat(":#{server.port}") unless default_ports "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" @@ -144,7 +144,7 @@ class JiraService < IssueTrackerService commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) - entity.last_commit.id + entity.diff_head_sha end commit_url = build_entity_url(:commit, commit_id) @@ -190,7 +190,6 @@ class JiraService < IssueTrackerService end end - def auth require 'base64' Base64.urlsafe_encode64("#{self.username}:#{self.password}") diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 11cce3e0561..f634e0772c0 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,5 +1,4 @@ class RedmineService < IssueTrackerService - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 73e736820af..0b700930641 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -22,12 +22,12 @@ class ProjectTeam end def find_member(user_id) - member = project.members.non_request.find_by(user_id: user_id) + member = project.members.find_by(user_id: user_id) # If user is not in project members # we should check for group membership if group && !member - member = group.members.non_request.find_by(user_id: user_id) + member = group.members.find_by(user_id: user_id) end member @@ -137,20 +137,10 @@ class ProjectTeam def max_member_access(user_id) access = [] - project.members.non_request.each do |member| - if member.user_id == user_id - access << member.access_field if member.access_field - break - end - end + access += project.members.where(user_id: user_id).has_access.pluck(:access_level) if group - group.members.non_request.each do |member| - if member.user_id == user_id - access << member.access_field if member.access_field - break - end - end + access += group.members.where(user_id: user_id).has_access.pluck(:access_level) end if project.invited_groups.any? && project.allowed_to_share_with_group? @@ -178,14 +168,14 @@ class ProjectTeam end def fetch_members(level = nil) - project_members = project.members.non_request - group_members = group ? group.members.non_request : [] + project_members = project.members + group_members = group ? group.members : [] invited_members = [] if project.invited_groups.any? && project.allowed_to_share_with_group? project.project_group_links.each do |group_link| invited_group = group_link.group - im = invited_group.members.non_request + im = invited_group.members if level int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 25d82929c0b..a255710f577 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -159,7 +159,7 @@ class ProjectWiki private def init_repo(path_with_namespace) - gitlab_shell.add_repository(path_with_namespace) + gitlab_shell.add_repository(project.repository_storage_path, path_with_namespace) end def commit_details(action, message = nil, title = nil) @@ -173,7 +173,7 @@ class ProjectWiki end def path_to_repo - @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") + @path_to_repo ||= File.join(project.repository_storage_path, "#{path_with_namespace}.git") end def update_project_activity diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 33cf046fa75..b7011d7afdf 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -8,4 +8,51 @@ class ProtectedBranch < ActiveRecord::Base def commit project.commit(self.name) end + + # Returns all protected branches that match the given branch name. + # This realizes all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_branches` to search + # through, to avoid calling out to the database. + def self.matching(branch_name, protected_branches: nil) + (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) } + end + + # Returns all branches (among the given list of branches [`Gitlab::Git::Branch`]) + # that match the current protected branch. + def matching(branches) + branches.select { |branch| self.matches?(branch.name) } + end + + # Checks if the protected branch matches the given branch name. + def matches?(branch_name) + return false if self.name.blank? + + exact_match?(branch_name) || wildcard_match?(branch_name) + end + + # Checks if this protected branch contains a wildcard + def wildcard? + self.name && self.name.include?('*') + end + + protected + + def exact_match?(branch_name) + self.name == branch_name + end + + def wildcard_match?(branch_name) + wildcard_regex === branch_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = self.name.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index bbd7682d8e7..1a2ac90da51 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -39,7 +39,7 @@ class Repository # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( - File.join(Gitlab.config.gitlab_shell.repos_path, path_with_namespace + ".git") + File.join(@project.repository_storage_path, path_with_namespace + ".git") ) end @@ -78,9 +78,9 @@ class Repository end end - def commit(id = 'HEAD') + def commit(ref = 'HEAD') return nil unless exists? - commit = Gitlab::Git::Commit.find(raw_repository, id) + commit = Gitlab::Git::Commit.find(raw_repository, ref) commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError @@ -130,7 +130,7 @@ class Repository end def find_tag(name) - raw_repository.tags.find { |tag| tag.name == name } + tags.find { |tag| tag.name == name } end def add_branch(user, branch_name, target) @@ -191,14 +191,38 @@ class Repository end end + def ref_names + branch_names + tag_names + end + def branch_names - cache.fetch(:branch_names) { branches.map(&:name) } + @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) } end def branch_exists?(branch_name) branch_names.include?(branch_name) end + def ref_exists?(ref) + rugged.references.exist?(ref) + end + + # Makes sure a commit is kept around when Git garbage collection runs. + # Git GC will delete commits from the repository that are no longer in any + # branches or tags, but we want to keep some of these commits around, for + # example if they have comments or CI builds. + def keep_around(sha) + return unless sha && commit(sha) + + return if kept_around?(sha) + + rugged.references.create(keep_around_ref_name(sha), sha) + end + + def kept_around?(sha) + ref_exists?(keep_around_ref_name(sha)) + end + def tag_names cache.fetch(:tag_names) { raw_repository.tag_names } end @@ -242,24 +266,26 @@ class Repository end end + # Keys for data that can be affected for any commit push. def cache_keys - %i(size branch_names tag_names branch_count tag_count commit_count + %i(size commit_count readme version contribution_guide changelog license_blob license_key gitignore) end + # Keys for data on branch/tag operations. + def cache_keys_for_branches_and_tags + %i(branch_names tag_names branch_count tag_count) + end + def build_cache - cache_keys.each do |key| + (cache_keys + cache_keys_for_branches_and_tags).each do |key| unless cache.exist?(key) send(key) end end end - def expire_gitignore - cache.expire(:gitignore) - end - def expire_tags_cache cache.expire(:tag_names) @tags = nil @@ -267,6 +293,7 @@ class Repository def expire_branches_cache cache.expire(:branch_names) + @branch_names = nil @local_branches = nil end @@ -281,8 +308,6 @@ class Repository # This ensures this particular cache is flushed after the first commit to a # new repository. expire_emptiness_caches if empty? - expire_branch_count_cache - expire_tag_count_cache end def expire_branch_cache(branch_name = nil) @@ -332,10 +357,6 @@ class Repository @lookup_cache ||= {} end - def expire_branch_names - cache.expire(:branch_names) - end - def expire_avatar_cache(branch_name = nil, revision = nil) # Avatars are pulled from the default branch, thus if somebody pushes to a # different branch there's no need to expire anything. @@ -632,16 +653,6 @@ class Repository end end - def blob_for_diff(commit, diff) - blob_at(commit.id, diff.file_path) - end - - def prev_blob_for_diff(commit, diff) - if commit.parent_id - blob_at(commit.parent_id, diff.old_path) - end - end - def refs_contains_sha(ref_type, sha) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first @@ -693,6 +704,7 @@ class Repository options[:commit] = { message: message, branch: ref, + update_ref: false, } raw_repository.mkdir(path, options) @@ -708,6 +720,7 @@ class Repository options[:commit] = { message: message, branch: ref, + update_ref: false, } options[:file] = { @@ -720,6 +733,33 @@ class Repository end end + def update_file(user, path, content, branch:, previous_path:, message:) + commit_with_hooks(user, branch) do |ref| + committer = user_to_committer(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref, + update_ref: false + } + + options[:file] = { + content: content, + path: path, + update: true + } + + if previous_path + options[:file][:previous_path] = previous_path + Gitlab::Git::Blob.rename(raw_repository, options) + else + Gitlab::Git::Blob.commit(raw_repository, options) + end + end + end + def remove_file(user, path, message, branch) commit_with_hooks(user, branch) do |ref| committer = user_to_committer(user) @@ -728,7 +768,8 @@ class Repository options[:author] = committer options[:commit] = { message: message, - branch: ref + branch: ref, + update_ref: false, } options[:file] = { @@ -758,9 +799,9 @@ class Repository end end - def merge(user, source_sha, target_branch, options = {}) - our_commit = rugged.branches[target_branch].target - their_commit = rugged.lookup(source_sha) + def merge(user, merge_request, options = {}) + our_commit = rugged.branches[merge_request.target_branch].target + their_commit = rugged.lookup(merge_request.diff_head_sha) raise "Invalid merge target" if our_commit.nil? raise "Invalid merge source" if their_commit.nil? @@ -768,14 +809,15 @@ class Repository merge_index = rugged.merge_commits(our_commit, their_commit) return false if merge_index.conflicts? - commit_with_hooks(user, target_branch) do |ref| + commit_with_hooks(user, merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), - update_ref: ref ) - Rugged::Commit.create(rugged, actual_options) + commit_id = Rugged::Commit.create(rugged, actual_options) + merge_request.update(in_progress_merge_commit_sha: commit_id) + commit_id end end @@ -785,15 +827,14 @@ class Repository return false unless revert_tree_id - commit_with_hooks(user, base_branch) do |ref| + commit_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, message: commit.revert_message, author: committer, committer: committer, tree: revert_tree_id, - parents: [rugged.lookup(source_sha)], - update_ref: ref) + parents: [rugged.lookup(source_sha)]) end end @@ -803,7 +844,7 @@ class Repository return false unless cherry_pick_tree_id - commit_with_hooks(user, base_branch) do |ref| + commit_with_hooks(user, base_branch) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, message: commit.message, @@ -814,8 +855,7 @@ class Repository }, committer: committer, tree: cherry_pick_tree_id, - parents: [rugged.lookup(source_sha)], - update_ref: ref) + parents: [rugged.lookup(source_sha)]) end end @@ -874,7 +914,6 @@ class Repository merge_base(ancestor_id, descendant_id) == ancestor_id end - def search_files(query, ref) offset = 2 args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) @@ -891,7 +930,7 @@ class Repository if line =~ /^.*:.*:\d+:/ ref, filename, startline = line.split(':') startline = startline.to_i - index - extname = File.extname(filename) + extname = Regexp.escape(File.extname(filename)) basename = filename.sub(/#{extname}$/, '') break end @@ -917,20 +956,6 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end - def with_tmp_ref(oldrev = nil) - random_string = SecureRandom.hex - tmp_ref = "refs/tmp/#{random_string}/head" - - if oldrev && !Gitlab::Git.blank_ref?(oldrev) - rugged.references.create(tmp_ref, oldrev) - end - - # Make commit in tmp ref - yield(tmp_ref) - ensure - rugged.references.delete(tmp_ref) rescue nil - end - def commit_with_hooks(current_user, branch) update_autocrlf_option @@ -943,33 +968,31 @@ class Repository oldrev = target_branch.target end - with_tmp_ref(oldrev) do |tmp_ref| - # Make commit in tmp ref - newrev = yield(tmp_ref) + # Make commit + newrev = yield(ref) - unless newrev - raise CommitError.new('Failed to create commit') - end + unless newrev + raise CommitError.new('Failed to create commit') + end - GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - if was_empty || !target_branch - # Create branch - rugged.references.create(ref, newrev) + GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do + if was_empty || !target_branch + # Create branch + rugged.references.create(ref, newrev) + else + # Update head + current_head = find_branch(branch).target + + # Make sure target branch was not changed during pre-receive hook + if current_head == oldrev + rugged.references.update(ref, newrev) else - # Update head - current_head = find_branch(branch).target - - # Make sure target branch was not changed during pre-receive hook - if current_head == oldrev - rugged.references.update(ref, newrev) - else - raise CommitError.new('Commit was rejected because branch received new push') - end + raise CommitError.new('Commit was rejected because branch received new push') end end - - newrev end + + newrev end def ls_files(ref) @@ -977,6 +1000,10 @@ class Repository raw_repository.ls_files(actual_ref) end + def gitattribute(path, name) + raw_repository.attributes(path)[name] + end + def copy_gitattributes(ref) actual_ref = ref || root_ref begin @@ -1014,4 +1041,8 @@ class Repository def tags_sorted_by_committed_date tags.sort_by { |tag| commit(tag.target).committed_date } end + + def keep_around_ref_name(sha) + "refs/keep-around/#{sha}" + end end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 375f195dba7..f4bcb49b34d 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -1,4 +1,6 @@ class SentNotification < ActiveRecord::Base + serialize :position, Gitlab::Diff::Position + belongs_to :project belongs_to :noteable, polymorphic: true belongs_to :recipient, class_name: "User" @@ -7,7 +9,9 @@ class SentNotification < ActiveRecord::Base validates :reply_key, uniqueness: true validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - validates :line_code, line_code: true, allow_blank: true + validate :note_valid + + after_save :keep_around_commit class << self def reply_key @@ -18,7 +22,7 @@ class SentNotification < ActiveRecord::Base find_by(reply_key: reply_key) end - def record(noteable, recipient_id, reply_key, params = {}) + def record(noteable, recipient_id, reply_key, attrs = {}) return unless reply_key noteable_id = nil @@ -29,7 +33,7 @@ class SentNotification < ActiveRecord::Base noteable_id = noteable.id end - params.reverse_merge!( + attrs.reverse_merge!( project: noteable.project, noteable_type: noteable.class.name, noteable_id: noteable_id, @@ -38,13 +42,17 @@ class SentNotification < ActiveRecord::Base reply_key: reply_key ) - create(params) + create(attrs) end - def record_note(note, recipient_id, reply_key, params = {}) - params[:line_code] = note.line_code + def record_note(note, recipient_id, reply_key, attrs = {}) + if note.diff_note? + attrs[:note_type] = note.type - record(note.noteable, recipient_id, reply_key, params) + attrs.merge!(note.diff_attributes) + end + + record(note.noteable, recipient_id, reply_key, attrs) end end @@ -64,7 +72,51 @@ class SentNotification < ActiveRecord::Base end end + def position=(new_position) + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + super(new_position) + end + def to_param self.reply_key end + + def note_attributes + { + project: self.project, + author: self.recipient, + type: self.note_type, + noteable_type: self.noteable_type, + noteable_id: self.noteable_id, + commit_id: self.commit_id, + line_code: self.line_code, + position: self.position.to_json + } + end + + def create_note(note) + Notes::CreateService.new( + self.project, + self.recipient, + self.note_attributes.merge(note: note) + ).execute + end + + private + + def note_valid + Note.new(note_attributes.merge(note: "Test")).valid? + end + + def keep_around_commit + project.repository.keep_around(self.commit_id) + end end diff --git a/app/models/service.rb b/app/models/service.rb index 40d39933ad8..5432f8c7ab4 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -76,6 +76,10 @@ class Service < ActiveRecord::Base [] end + def test_data(project, user) + Gitlab::PushDataBuilder.build_sample(project, user) + end + def supported_events %w(push tag_push issue merge_request wiki_page) end @@ -94,6 +98,11 @@ class Service < ActiveRecord::Base !project.empty_repo? end + # reason why service cannot be tested + def disabled_title + "Please setup a project repository." + end + # Provide convenient accessor methods # for each serialized property. # Also keep track of updated properties in a similar way as ActiveModel::Dirty @@ -170,6 +179,7 @@ class Service < ActiveRecord::Base bamboo buildkite builds_email + bugzilla campfire custom_issue_tracker drone_ci diff --git a/app/models/snippet.rb b/app/models/snippet.rb index f8034cb5e6b..5ec933601ac 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -20,6 +20,7 @@ class Snippet < ActiveRecord::Base length: { within: 0..255 }, format: { with: Gitlab::Regex.file_name_regex, message: Gitlab::Regex.file_name_regex_message } + validates :content, presence: true validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } @@ -81,6 +82,11 @@ class Snippet < ActiveRecord::Base 0 end + # alias for compatibility with blobs and highlighting + def path + file_name + end + def name file_name end @@ -135,7 +141,16 @@ class Snippet < ActiveRecord::Base end def accessible_to(user) - where('visibility_level IN (?) OR author_id = ?', [Snippet::INTERNAL, Snippet::PUBLIC], user) + return are_public unless user.present? + return all if user.admin? + + where( + 'visibility_level IN (:visibility_levels) + OR author_id = :author_id + OR project_id IN (:project_ids)', + visibility_levels: [Snippet::PUBLIC, Snippet::INTERNAL], + author_id: user.id, + project_ids: user.authorized_projects.select(:id)) end end end diff --git a/app/models/todo.rb b/app/models/todo.rb index 2792fa9b9a8..8d7a5965aa1 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,8 +1,17 @@ class Todo < ActiveRecord::Base - ASSIGNED = 1 - MENTIONED = 2 - BUILD_FAILED = 3 - MARKED = 4 + ASSIGNED = 1 + MENTIONED = 2 + BUILD_FAILED = 3 + MARKED = 4 + APPROVAL_REQUIRED = 5 # This is an EE-only feature + + ACTION_NAMES = { + ASSIGNED => :assigned, + MENTIONED => :mentioned, + BUILD_FAILED => :build_failed, + MARKED => :marked, + APPROVAL_REQUIRED => :approval_required + } belongs_to :author, class_name: "User" belongs_to :note @@ -30,10 +39,16 @@ class Todo < ActiveRecord::Base state :done end + after_save :keep_around_commit + def build_failed? action == BUILD_FAILED end + def action_name + ACTION_NAMES[action] + end + def body if note.present? note.note @@ -62,4 +77,10 @@ class Todo < ActiveRecord::Base target.to_reference end end + + private + + def keep_around_commit + project.repository.keep_around(self.commit_id) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 2e458329cb9..975e935fa20 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,7 +15,7 @@ class User < ActiveRecord::Base add_authentication_token_field :authentication_token default_value_for :admin, false - default_value_for :external, false + default_value_for(:external) { current_application_settings.user_default_external } default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false @@ -25,6 +25,7 @@ class User < ActiveRecord::Base attr_encrypted :otp_secret, key: Gitlab::Application.config.secret_key_base, mode: :per_attribute_iv_and_salt, + insecure_mode: true, algorithm: 'aes-256-cbc' devise :two_factor_authenticatable, @@ -57,7 +58,7 @@ class User < ActiveRecord::Base # Groups has_many :members, dependent: :destroy - has_many :group_members, dependent: :destroy, source: 'GroupMember' + has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' has_many :groups, through: :group_members has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group @@ -65,7 +66,7 @@ class User < ActiveRecord::Base # Projects has_many :groups_projects, through: :groups, source: :projects has_many :personal_projects, through: :namespace, source: :projects - has_many :project_members, dependent: :destroy, class_name: 'ProjectMember' + has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember' has_many :projects, through: :project_members has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy @@ -84,9 +85,10 @@ class User < ActiveRecord::Base has_one :abuse_report, dependent: :destroy has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' + has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy - has_many :award_emoji, as: :awardable, dependent: :destroy + has_many :award_emoji, dependent: :destroy # # Validations @@ -308,7 +310,7 @@ class User < ActiveRecord::Base def generate_password if self.force_random_password - self.password = self.password_confirmation = Devise.friendly_token.first(8) + self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min) end end @@ -487,9 +489,8 @@ class User < ActiveRecord::Base events.recent.find do |event| project = Project.find_by_id(event.project_id) next unless project - repo = project.repository - if repo.branch_names.include?(event.branch_name) + if project.repository.branch_exists?(event.branch_name) merge_requests = MergeRequest.where("created_at >= ?", event.created_at). where(source_project_id: project.id, source_branch: event.branch_name) @@ -653,7 +654,7 @@ class User < ActiveRecord::Base end def avatar_url(size = nil, scale = 2) - if avatar.present? + if self[:avatar].present? [gitlab_config.url, avatar.url].join else GravatarService.new.execute(email, size, scale) @@ -764,7 +765,7 @@ class User < ActiveRecord::Base unless email_domains.blank? match_found = email_domains.any? do |domain| - escaped = Regexp.escape(domain).gsub('\*','.*?') + escaped = Regexp.escape(domain).gsub('\*', '.*?') regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE email_domain = Mail::Address.new(self.email).domain email_domain =~ regexp @@ -852,9 +853,8 @@ class User < ActiveRecord::Base projects.select(:id), groups.joins(:shared_projects).select(:project_id)] - if min_access_level - scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } } + scope = { access_level: Gitlab::Access.all_values.select { |access| access >= min_access_level } } relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) } end |