summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2016-07-19 23:33:42 +0200
committerKamil Trzcinski <ayufan@ayufan.eu>2016-07-19 23:33:42 +0200
commitbf9db45a1dc4d8269fa68dc27dac6667c89ffda8 (patch)
tree813d6c8151c156d186cdf4e3420f42260d206434 /app/models
parent0aedeb5637932fa827e42be7441e9c967049dd1d (diff)
parentb9ed9d658ad447a64d58b2040849a7cc0e698287 (diff)
downloadgitlab-ce-bf9db45a1dc4d8269fa68dc27dac6667c89ffda8.tar.gz
Merge remote-tracking branch 'origin/master' into ci-predefined-variables
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb48
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/award_emoji.rb2
-rw-r--r--app/models/ci/build.rb97
-rw-r--r--app/models/ci/pipeline.rb41
-rw-r--r--app/models/ci/runner.rb23
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/ci/variable.rb1
-rw-r--r--app/models/commit.rb33
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/concerns/awardable.rb21
-rw-r--r--app/models/concerns/issuable.rb40
-rw-r--r--app/models/concerns/mentionable.rb10
-rw-r--r--app/models/concerns/note_on_diff.rb52
-rw-r--r--app/models/concerns/participable.rb17
-rw-r--r--app/models/concerns/statuseable.rb6
-rw-r--r--app/models/deployment.rb10
-rw-r--r--app/models/diff_note.rb127
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/group.rb9
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/label.rb19
-rw-r--r--app/models/legacy_diff_note.rb73
-rw-r--r--app/models/member.rb10
-rw-r--r--app/models/members/group_member.rb12
-rw-r--r--app/models/members/project_member.rb13
-rw-r--r--app/models/merge_request.rb236
-rw-r--r--app/models/merge_request_diff.rb184
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/namespace.rb95
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb43
-rw-r--r--app/models/notification_setting.rb9
-rw-r--r--app/models/project.rb90
-rw-r--r--app/models/project_import_data.rb1
-rw-r--r--app/models/project_services/bugzilla_service.rb23
-rw-r--r--app/models/project_services/builds_email_service.rb27
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb5
-rw-r--r--app/models/project_services/drone_ci_service.rb1
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb10
-rw-r--r--app/models/project_services/issue_tracker_service.rb1
-rw-r--r--app/models/project_services/jira_service.rb5
-rw-r--r--app/models/project_services/redmine_service.rb1
-rw-r--r--app/models/project_team.rb24
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb47
-rw-r--r--app/models/repository.rb185
-rw-r--r--app/models/sent_notification.rb66
-rw-r--r--app/models/service.rb10
-rw-r--r--app/models/snippet.rb17
-rw-r--r--app/models/todo.rb29
-rw-r--r--app/models/user.rb22
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