summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb26
-rw-r--r--app/models/blob.rb11
-rw-r--r--app/models/ci/build.rb54
-rw-r--r--app/models/ci/pipeline.rb131
-rw-r--r--app/models/commit.rb47
-rw-r--r--app/models/commit_status.rb37
-rw-r--r--app/models/compare.rb66
-rw-r--r--app/models/concerns/faster_cache_keys.rb16
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/spammable.rb68
-rw-r--r--app/models/concerns/statuseable.rb31
-rw-r--r--app/models/concerns/token_authenticatable.rb30
-rw-r--r--app/models/deployment.rb6
-rw-r--r--app/models/diff_note.rb12
-rw-r--r--app/models/discussion.rb6
-rw-r--r--app/models/environment.rb18
-rw-r--r--app/models/issue.rb38
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/label_link.rb6
-rw-r--r--app/models/legacy_diff_note.rb14
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/project_member.rb7
-rw-r--r--app/models/merge_request.rb42
-rw-r--r--app/models/merge_request_diff.rb14
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb60
-rw-r--r--app/models/project_services/campfire_service.rb51
-rw-r--r--app/models/project_services/hipchat_service.rb21
-rw-r--r--app/models/project_services/pivotaltracker_service.rb31
-rw-r--r--app/models/project_team.rb77
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb6
-rw-r--r--app/models/protected_branch/merge_access_level.rb24
-rw-r--r--app/models/protected_branch/push_access_level.rb27
-rw-r--r--app/models/repository.rb50
-rw-r--r--app/models/spam_log.rb4
-rw-r--r--app/models/spam_report.rb5
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/user_agent_detail.rb9
40 files changed, 828 insertions, 243 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index f33c8d61d3f..d9113ffd99a 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -6,6 +6,10 @@ class Ability
return [] unless user.is_a?(User)
return [] if user.blocked?
+ abilities_by_subject_class(user: user, subject: subject)
+ end
+
+ def abilities_by_subject_class(user:, subject:)
case subject
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
@@ -47,6 +51,16 @@ class Ability
end
end
+ # Returns an Array of Issues that can be read by the given user.
+ #
+ # issues - The issues to reduce down to those readable by the user.
+ # user - The User for which to check the issues
+ def issues_readable_by_user(issues, user = nil)
+ return issues if user && user.admin?
+
+ issues.select { |issue| issue.visible_to_user?(user) }
+ end
+
# List of possible abilities for anonymous user
def anonymous_abilities(user, subject)
if subject.is_a?(PersonalSnippet)
@@ -388,6 +402,18 @@ class Ability
GroupProjectsFinder.new(group).execute(user).any?
end
+ def can_edit_note?(user, note)
+ return false if !note.editable? || !user.present?
+ return true if note.author == user || user.admin?
+
+ if note.project
+ max_access_level = note.project.team.max_member_access(user.id)
+ max_access_level >= Gitlab::Access::MASTER
+ else
+ false
+ end
+ end
+
def namespace_abilities(user, namespace)
rules = []
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 4279ea2ce57..12cc5aaafba 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -3,6 +3,9 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
+ # The maximum size of an SVG that can be displayed.
+ MAXIMUM_SVG_SIZE = 2.megabytes
+
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
# This method prevents the decorated object from evaluating to "truthy" when
@@ -31,6 +34,14 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
+ def size_within_svg_limits?
+ size <= MAXIMUM_SVG_SIZE
+ end
+
+ def video?
+ UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
+ end
+
def to_partial_path
if lfs_pointer?
'download'
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index cbfa14e81f1..3d6c6ea3209 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -13,9 +13,10 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) }
+ scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
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) }
+ scope :manual_actions, ->() { where(when: :manual).relevant }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
@@ -41,40 +42,35 @@ module Ci
end
def retry(build, user = nil)
- new_build = Ci::Build.new(status: 'pending')
- new_build.ref = build.ref
- new_build.tag = build.tag
- new_build.options = build.options
- new_build.commands = build.commands
- new_build.tag_list = build.tag_list
- new_build.project = build.project
- new_build.pipeline = build.pipeline
- new_build.name = build.name
- new_build.allow_failure = build.allow_failure
- 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
+ new_build = Ci::Build.create(
+ ref: build.ref,
+ tag: build.tag,
+ options: build.options,
+ commands: build.commands,
+ tag_list: build.tag_list,
+ project: build.project,
+ pipeline: build.pipeline,
+ name: build.name,
+ allow_failure: build.allow_failure,
+ stage: build.stage,
+ stage_idx: build.stage_idx,
+ trigger_request: build.trigger_request,
+ yaml_variables: build.yaml_variables,
+ when: build.when,
+ user: user,
+ environment: build.environment,
+ status_event: 'enqueue'
+ )
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
end
end
- state_machine :status, initial: :pending do
+ state_machine :status do
after_transition pending: :running do |build|
build.execute_hooks
end
- # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
- around_transition any => [:success, :failed, :canceled] do |build, block|
- block.call
- build.pipeline.create_next_builds(build) if build.pipeline
- end
-
after_transition any => [:success, :failed, :canceled] do |build|
build.update_coverage
build.execute_hooks
@@ -106,7 +102,7 @@ module Ci
def play(current_user = nil)
# Try to queue a current build
- if self.queue
+ if self.enqueue
self.update(user: current_user)
self
else
@@ -331,7 +327,7 @@ module Ci
end
def valid_token?(token)
- project.valid_runners_token? token
+ project.valid_runners_token?(token)
end
def has_tags?
@@ -460,7 +456,7 @@ module Ci
def build_attributes_from_config
return {} unless pipeline.config_processor
-
+
pipeline.config_processor.build_attributes(name)
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index bce6a992af6..8cfba92ae9b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -13,13 +13,51 @@ module Ci
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha
+ validates_presence_of :ref
validates_presence_of :status
validate :valid_commit_sha
- # Invalidate object and save if when touched
- after_touch :update_state
after_save :keep_around_commits
+ state_machine :status, initial: :created do
+ event :enqueue do
+ transition created: :pending
+ transition [:success, :failed, :canceled, :skipped] => :running
+ end
+
+ event :run do
+ transition any => :running
+ end
+
+ event :skip do
+ transition any => :skipped
+ end
+
+ event :drop do
+ transition any => :failed
+ end
+
+ event :succeed do
+ transition any => :success
+ end
+
+ event :cancel do
+ transition any => :canceled
+ end
+
+ before_transition [:created, :pending] => :running do |pipeline|
+ pipeline.started_at = Time.now
+ end
+
+ before_transition any => [:success, :failed, :canceled] do |pipeline|
+ pipeline.finished_at = Time.now
+ end
+
+ before_transition do |pipeline|
+ pipeline.update_duration
+ end
+ end
+
# ref can't be HEAD or SHA, can only be branch/tag name
scope :latest_successful_for, ->(ref = default_branch) do
where(ref: ref).success.order(id: :desc).limit(1)
@@ -109,37 +147,6 @@ module Ci
trigger_requests.any?
end
- def create_builds(user, trigger_request = nil)
- ##
- # We persist pipeline only if there are builds available
- #
- return unless config_processor
-
- build_builds_for_stages(config_processor.stages, user,
- 'success', trigger_request) && save
- end
-
- def create_next_builds(build)
- return unless config_processor
-
- # don't create other builds if this one is retried
- latest_builds = builds.latest
- return unless latest_builds.exists?(build.id)
-
- # get list of stages after this build
- next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
- next_stages.delete(build.stage)
-
- # get status for all prior builds
- prior_builds = latest_builds.where.not(stage: next_stages)
- prior_status = prior_builds.status
-
- # build builds for next stage that has builds available
- # and save pipeline if we have builds
- build_builds_for_stages(next_stages, build.user, prior_status,
- build.trigger_request) && save
- end
-
def retried
@retried ||= (statuses.order(id: :desc) - statuses.latest)
end
@@ -151,6 +158,14 @@ module Ci
end
end
+ def config_builds_attributes
+ return [] unless config_processor
+
+ config_processor.
+ builds_for_ref(ref, tag?, trigger_requests.first).
+ sort_by { |build| build[:stage_idx] }
+ end
+
def has_warnings?
builds.latest.ignored.any?
end
@@ -182,10 +197,6 @@ module Ci
end
end
- def skip_ci?
- 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
@@ -207,37 +218,37 @@ module Ci
Note.for_commit_id(sha)
end
+ def process!
+ Ci::ProcessPipelineService.new(project, user).execute(self)
+ end
+
+ def build_updated
+ case latest_builds_status
+ when 'pending' then enqueue
+ when 'running' then run
+ when 'success' then succeed
+ when 'failed' then drop
+ when 'canceled' then cancel
+ when 'skipped' then skip
+ end
+ end
+
def predefined_variables
[
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
]
end
+ def update_duration
+ self.duration = statuses.latest.duration
+ end
+
private
- def build_builds_for_stages(stages, user, status, trigger_request)
- ##
- # Note that `Array#any?` implements a short circuit evaluation, so we
- # build builds only for the first stage that has builds available.
- #
- stages.any? do |stage|
- CreateBuildsService.new(self).
- execute(stage, user, status, trigger_request).
- any?(&:active?)
- end
- end
-
- def update_state
- statuses.reload
- self.status = if yaml_errors.blank?
- statuses.latest.status || 'skipped'
- else
- 'failed'
- end
- self.started_at = statuses.started_at
- self.finished_at = statuses.finished_at
- self.duration = statuses.latest.duration
- save
+ def latest_builds_status
+ return 'failed' unless yaml_errors.blank?
+
+ statuses.latest.status || 'skipped'
end
def keep_around_commits
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 2ef3973c160..cc413448ce8 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -104,7 +104,7 @@ class Commit
end
def diff_line_count
- @diff_line_count ||= Commit::diff_line_count(self.diffs)
+ @diff_line_count ||= Commit::diff_line_count(raw_diffs)
@diff_line_count
end
@@ -123,15 +123,17 @@ class Commit
# In case this first line is longer than 100 characters, it is cut off
# after 80 characters and ellipses (`&hellp;`) are appended.
def title
- title = safe_message
+ full_title.length > 100 ? full_title[0..79] << "…" : full_title
+ end
- return no_commit_message if title.blank?
+ # Returns the full commits title
+ def full_title
+ return @full_title if @full_title
- title_end = title.index("\n")
- if (!title_end && title.length > 100) || (title_end && title_end > 100)
- title[0..79] << "…"
+ if safe_message.blank?
+ @full_title = no_commit_message
else
- title.split("\n", 2).first
+ @full_title = safe_message.split("\n", 2).first
end
end
@@ -178,7 +180,18 @@ class Commit
end
def author
- @author ||= User.find_by_any_email(author_email.downcase)
+ if RequestStore.active?
+ key = "commit_author:#{author_email.downcase}"
+ # nil is a valid value since no author may exist in the system
+ if RequestStore.store.has_key?(key)
+ @author = RequestStore.store[key]
+ else
+ @author = find_author_by_any_email
+ RequestStore.store[key] = @author
+ end
+ else
+ @author ||= find_author_by_any_email
+ end
end
def committer
@@ -295,8 +308,8 @@ class Commit
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
+ blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
+ blob.image? || blob.video? ? :raw : :blob
else
entry[:type]
end
@@ -304,12 +317,24 @@ class Commit
nil
end
+ def raw_diffs(*args)
+ raw.diffs(*args)
+ end
+
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
+ end
+
private
+ def find_author_by_any_email
+ User.find_by_any_email(author_email.downcase)
+ end
+
def repo_changes
changes = { added: [], modified: [], removed: [] }
- diffs.each do |diff|
+ raw_diffs(deltas_only: true).each do |diff|
if diff.deleted_file
changes[:removed] << diff.old_path
elsif diff.renamed_file || diff.new_file
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2d185c28809..703ca90edb6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
delegate :commit, to: :pipeline
@@ -25,28 +25,36 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
- state_machine :status, initial: :pending do
- event :queue do
- transition skipped: :pending
+ state_machine :status do
+ event :enqueue do
+ transition [:created, :skipped] => :pending
end
event :run do
transition pending: :running
end
+ event :skip do
+ transition [:created, :pending] => :skipped
+ end
+
event :drop do
- transition [:pending, :running] => :failed
+ transition [:created, :pending, :running] => :failed
end
event :success do
- transition [:pending, :running] => :success
+ transition [:created, :pending, :running] => :success
end
event :cancel do
- transition [:pending, :running] => :canceled
+ transition [:created, :pending, :running] => :canceled
end
- after_transition pending: :running do |commit_status|
+ after_transition created: [:pending, :running] do |commit_status|
+ commit_status.update_attributes queued_at: Time.now
+ end
+
+ after_transition [:created, :pending] => :running do |commit_status|
commit_status.update_attributes started_at: Time.now
end
@@ -54,7 +62,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- after_transition [:pending, :running] => :success do |commit_status|
+ # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
+ around_transition any => [:success, :failed, :canceled] do |commit_status, block|
+ block.call
+
+ commit_status.pipeline.try(:process!)
+ end
+
+ after_transition do |commit_status, transition|
+ commit_status.pipeline.try(:build_updated) unless transition.loopback?
+ end
+
+ after_transition [:created, :pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
end
diff --git a/app/models/compare.rb b/app/models/compare.rb
new file mode 100644
index 00000000000..4856510f526
--- /dev/null
+++ b/app/models/compare.rb
@@ -0,0 +1,66 @@
+class Compare
+ delegate :same, :head, :base, to: :@compare
+
+ attr_reader :project
+
+ def self.decorate(compare, project)
+ if compare.is_a?(Compare)
+ compare
+ else
+ self.new(compare, project)
+ end
+ end
+
+ def initialize(compare, project)
+ @compare = compare
+ @project = project
+ end
+
+ def commits
+ @commits ||= Commit.decorate(@compare.commits, project)
+ end
+
+ def start_commit
+ return @start_commit if defined?(@start_commit)
+
+ commit = @compare.base
+ @start_commit = commit ? ::Commit.new(commit, project) : nil
+ end
+
+ def head_commit
+ return @head_commit if defined?(@head_commit)
+
+ commit = @compare.head
+ @head_commit = commit ? ::Commit.new(commit, project) : nil
+ end
+ alias_method :commit, :head_commit
+
+ def base_commit
+ return @base_commit if defined?(@base_commit)
+
+ @base_commit = if start_commit && head_commit
+ project.merge_base_commit(start_commit.id, head_commit.id)
+ else
+ nil
+ end
+ end
+
+ def raw_diffs(*args)
+ @compare.diffs(*args)
+ end
+
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::Compare.new(self,
+ project: project,
+ diff_options: diff_options,
+ diff_refs: diff_refs)
+ end
+
+ def diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: base_commit.try(:sha),
+ start_sha: start_commit.try(:sha),
+ head_sha: commit.try(:sha)
+ )
+ end
+end
diff --git a/app/models/concerns/faster_cache_keys.rb b/app/models/concerns/faster_cache_keys.rb
new file mode 100644
index 00000000000..5b14723fa2d
--- /dev/null
+++ b/app/models/concerns/faster_cache_keys.rb
@@ -0,0 +1,16 @@
+module FasterCacheKeys
+ # A faster version of Rails' "cache_key" method.
+ #
+ # Rails' default "cache_key" method uses all kind of complex logic to figure
+ # out the cache key. In many cases this complexity and overhead may not be
+ # needed.
+ #
+ # This method does not do any timestamp parsing as this process is quite
+ # expensive and not needed when generating cache keys. This method also relies
+ # on the table name instead of the cache namespace name as the latter uses
+ # complex logic to generate the exact same value (as when using the table
+ # name) in 99% of the cases.
+ def cache_key
+ "#{self.class.table_name}/#{id}-#{read_attribute_before_type_cast(:updated_at)}"
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index acb6f5a2998..cbae1cd439b 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -17,7 +17,7 @@ module Issuable
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :milestone
- has_many :notes, as: :noteable, dependent: :destroy do
+ has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do
def authors_loaded?
# We check first if we're loaded to not load unnecessarily.
loaded? && to_a.all? { |note| note.association(:author).loaded? }
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
new file mode 100644
index 00000000000..ce54fe5d3bf
--- /dev/null
+++ b/app/models/concerns/spammable.rb
@@ -0,0 +1,68 @@
+module Spammable
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def attr_spammable(attr, options = {})
+ spammable_attrs << [attr.to_s, options]
+ end
+ end
+
+ included do
+ has_one :user_agent_detail, as: :subject, dependent: :destroy
+
+ attr_accessor :spam
+
+ after_validation :check_for_spam, on: :create
+
+ cattr_accessor :spammable_attrs, instance_accessor: false do
+ []
+ end
+
+ delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
+ end
+
+ def submittable_as_spam?
+ if user_agent_detail
+ user_agent_detail.submittable?
+ else
+ false
+ end
+ end
+
+ def spam?
+ @spam
+ end
+
+ def check_for_spam
+ self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
+ end
+
+ def spam_title
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_title, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spam_description
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_description, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spammable_text
+ result = self.class.spammable_attrs.map do |attr|
+ public_send(attr.first)
+ end
+
+ result.reject(&:blank?).join("\n")
+ end
+
+ # Override in Spammable if further checks are necessary
+ def check_for_spam?
+ true
+ end
+end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
index 44c6b30f278..5d4b0a86899 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/statuseable.rb
@@ -1,18 +1,22 @@
module Statuseable
extend ActiveSupport::Concern
- AVAILABLE_STATUSES = %w(pending running success failed canceled skipped)
+ AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
+ STARTED_STATUSES = %w[running success failed skipped]
+ ACTIVE_STATUSES = %w[pending running]
+ COMPLETED_STATUSES = %w[success failed canceled]
class_methods do
def status_sql
- builds = all.select('count(*)').to_sql
- success = all.success.select('count(*)').to_sql
- ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored)
+ scope = all.relevant
+ builds = scope.select('count(*)').to_sql
+ success = scope.success.select('count(*)').to_sql
+ ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
ignored ||= '0'
- pending = all.pending.select('count(*)').to_sql
- running = all.running.select('count(*)').to_sql
- canceled = all.canceled.select('count(*)').to_sql
- skipped = all.skipped.select('count(*)').to_sql
+ pending = scope.pending.select('count(*)').to_sql
+ running = scope.running.select('count(*)').to_sql
+ canceled = scope.canceled.select('count(*)').to_sql
+ skipped = scope.skipped.select('count(*)').to_sql
deduce_status = "(CASE
WHEN (#{builds})=0 THEN NULL
@@ -48,7 +52,8 @@ module Statuseable
included do
validates :status, inclusion: { in: AVAILABLE_STATUSES }
- state_machine :status, initial: :pending do
+ state_machine :status, initial: :created do
+ state :created, value: 'created'
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
@@ -57,6 +62,8 @@ module Statuseable
state :skipped, value: 'skipped'
end
+ scope :created, -> { where(status: 'created') }
+ scope :relevant, -> { where.not(status: 'created') }
scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
@@ -68,14 +75,14 @@ module Statuseable
end
def started?
- !pending? && !canceled? && started_at
+ STARTED_STATUSES.include?(status) && started_at
end
def active?
- running? || pending?
+ ACTIVE_STATUSES.include?(status)
end
def complete?
- canceled? || success? || failed?
+ COMPLETED_STATUSES.include?(status)
end
end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 885deaf78d2..24c7b26d223 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -1,12 +1,26 @@
module TokenAuthenticatable
extend ActiveSupport::Concern
+ private
+
+ def write_new_token(token_field)
+ new_token = generate_token(token_field)
+ write_attribute(token_field, new_token)
+ end
+
+ def generate_token(token_field)
+ loop do
+ token = Devise.friendly_token
+ break token unless self.class.unscoped.find_by(token_field => token)
+ end
+ end
+
class_methods do
def authentication_token_fields
@token_fields || []
end
- private
+ private # rubocop:disable Lint/UselessAccessModifier
def add_authentication_token_field(token_field)
@token_fields = [] unless @token_fields
@@ -32,18 +46,4 @@ module TokenAuthenticatable
end
end
end
-
- private
-
- def write_new_token(token_field)
- new_token = generate_token(token_field)
- write_attribute(token_field, new_token)
- end
-
- def generate_token(token_field)
- loop do
- token = Devise.friendly_token
- break token unless self.class.unscoped.find_by(token_field => token)
- end
- end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1a7cd60817e..1e338889714 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -36,4 +36,10 @@ class Deployment < ActiveRecord::Base
def manual_actions
deployable.try(:other_actions)
end
+
+ def includes_commit?(commit)
+ return false unless commit
+
+ project.repository.is_ancestor?(commit.id, sha)
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 9671955db36..c816deb4e0c 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -67,7 +67,7 @@ class DiffNote < Note
return false unless supported?
return true if for_commit?
- diff_refs ||= self.noteable.diff_refs
+ diff_refs ||= noteable_diff_refs
self.position.diff_refs == diff_refs
end
@@ -78,6 +78,14 @@ class DiffNote < Note
!self.for_merge_request? || self.noteable.support_new_diff_notes?
end
+ def noteable_diff_refs
+ if noteable.respond_to?(:diff_sha_refs)
+ noteable.diff_sha_refs
+ else
+ noteable.diff_refs
+ end
+ end
+
def set_original_position
self.original_position = self.position.dup
end
@@ -96,7 +104,7 @@ class DiffNote < Note
self.project,
nil,
old_diff_refs: self.position.diff_refs,
- new_diff_refs: self.noteable.diff_refs,
+ new_diff_refs: noteable_diff_refs,
paths: self.position.paths
).execute(self)
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 74facfd1c9c..e2218a5f02b 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -49,6 +49,12 @@ class Discussion
self.noteable == target && !diff_discussion?
end
+ def active?
+ return @active if defined?(@active)
+
+ @active = first_note.active?
+ end
+
def expanded?
!diff_discussion? || active?
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index ac3a571a1f3..75e6f869786 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -3,6 +3,8 @@ class Environment < ActiveRecord::Base
has_many :deployments
+ before_validation :nullify_external_url
+
validates :name,
presence: true,
uniqueness: { scope: :project_id },
@@ -10,7 +12,23 @@ class Environment < ActiveRecord::Base
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
+ validates :external_url,
+ uniqueness: { scope: :project_id },
+ length: { maximum: 255 },
+ allow_nil: true,
+ addressable_url: true
+
def last_deployment
deployments.last
end
+
+ def nullify_external_url
+ self.external_url = nil if self.external_url.blank?
+ end
+
+ def includes_commit?(commit)
+ return false unless last_deployment
+
+ last_deployment.includes_commit?(commit)
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 60af8c15340..788611305fe 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -6,6 +6,8 @@ class Issue < ActiveRecord::Base
include Referable
include Sortable
include Taskable
+ include Spammable
+ include FasterCacheKeys
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -34,6 +36,9 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ attr_spammable :title, spam_title: true
+ attr_spammable :description, spam_description: true
+
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
@@ -229,7 +234,40 @@ class Issue < ActiveRecord::Base
self.closed_by_merge_requests(current_user).empty?
end
+ # Returns `true` if the current issue can be viewed by either a logged in User
+ # or an anonymous user.
+ def visible_to_user?(user = nil)
+ user ? readable_by?(user) : publicly_visible?
+ end
+
+ # Returns `true` if the given User can read the current Issue.
+ def readable_by?(user)
+ if user.admin?
+ true
+ elsif project.owner == user
+ true
+ elsif confidential?
+ author == user ||
+ assignee == user ||
+ project.team.member?(user, Gitlab::Access::REPORTER)
+ else
+ project.public? ||
+ project.internal? && !user.external? ||
+ project.team.member?(user)
+ end
+ end
+
+ # Returns `true` if this Issue is visible to everybody.
+ def publicly_visible?
+ project.public? && !confidential?
+ end
+
def overdue?
due_date.try(:past?) || false
end
+
+ # Only issues on public projects should be checked for spam
+ def check_for_spam?
+ project.public?
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index b9bc38a0436..568a60b8af3 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -26,8 +26,9 @@ class Key < ActiveRecord::Base
end
def publishable_key
- # Removes anything beyond the keytype and key itself
- self.key.split[0..1].join(' ')
+ # Strip out the keys comment so we don't leak email addresses
+ # Replace with simple ident of user_name (hostname)
+ self.key.split[0..1].push("#{self.user_name} (#{Gitlab.config.gitlab.host})").join(' ')
end
# projects that has this key
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 47bd6eaf35f..51b5c2b1f4c 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -1,7 +1,9 @@
class LabelLink < ActiveRecord::Base
+ include Importable
+
belongs_to :target, polymorphic: true
belongs_to :label
- validates :target, presence: true
- validates :label, presence: true
+ validates :target, presence: true, unless: :importing?
+ validates :label, presence: true, unless: :importing?
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 04a651d50ab..6ed66001513 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -25,6 +25,14 @@ class LegacyDiffNote < Note
@discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
end
+ def project_repository
+ if RequestStore.active?
+ RequestStore.fetch("project:#{project_id}:repository") { self.project.repository }
+ else
+ self.project.repository
+ end
+ end
+
def diff_file_hash
line_code.split('_')[0] if line_code
end
@@ -34,7 +42,7 @@ class LegacyDiffNote < Note
end
def diff_file
- @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff
+ @diff_file ||= Gitlab::Diff::File.new(diff, repository: project_repository) if diff
end
def diff_line
@@ -77,7 +85,7 @@ class LegacyDiffNote < Note
return nil unless noteable
return @diff if defined?(@diff)
- @diff = noteable.diffs(Commit.max_diff_options).find do |d|
+ @diff = noteable.raw_diffs(Commit.max_diff_options).find do |d|
d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash
end
end
@@ -108,7 +116,7 @@ class LegacyDiffNote < Note
# Find the diff on noteable that matches our own
def find_noteable_diff
- diffs = noteable.diffs(Commit.max_diff_options)
+ diffs = noteable.raw_diffs(Commit.max_diff_options)
diffs.find { |d| d.new_path == self.diff.new_path }
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 44db3d977fa..24ab1276ee9 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -53,6 +53,10 @@ class Member < ActiveRecord::Base
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
+ def access_for_user_ids(user_ids)
+ where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
+ end
+
def find_by_invite_token(invite_token)
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
find_by(invite_token: invite_token)
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index f39afc61ce9..18e97c969d7 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -8,6 +8,7 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
validates_format_of :source_type, with: /\AProject\z/
+ validates :access_level, inclusion: { in: Gitlab::Access.values }
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
@@ -21,19 +22,19 @@ class ProjectMember < Member
# or symbol like :master representing role
#
# Ex.
- # add_users_into_projects(
+ # add_users_to_projects(
# project_ids,
# user_ids,
# ProjectMember::MASTER
# )
#
- # add_users_into_projects(
+ # add_users_to_projects(
# project_ids,
# user_ids,
# :master
# )
#
- def add_users_into_projects(project_ids, user_ids, access, current_user = nil)
+ def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index dc758a45bcf..d6a6a9a11ae 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -104,6 +104,7 @@ class MergeRequest < ActiveRecord::Base
scope :from_project, ->(project) { where(source_project_id: project.id) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
+ scope :from_source_branches, ->(branches) { where(source_branch: branches) }
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
@@ -164,8 +165,16 @@ class MergeRequest < ActiveRecord::Base
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)
+ def raw_diffs(*args)
+ merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
+ end
+
+ def diffs(diff_options = nil)
+ if self.compare
+ self.compare.diffs(diff_options)
+ else
+ Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options)
+ end
end
def diff_size
@@ -238,11 +247,11 @@ class MergeRequest < ActiveRecord::Base
end
def target_branch_sha
- target_branch_head.try(:sha)
+ @target_branch_sha || target_branch_head.try(:sha)
end
def source_branch_sha
- source_branch_head.try(:sha)
+ @source_branch_sha || source_branch_head.try(:sha)
end
def diff_refs
@@ -255,6 +264,19 @@ class MergeRequest < ActiveRecord::Base
)
end
+ # Return diff_refs instance trying to not touch the git repository
+ def diff_sha_refs
+ if merge_request_diff && merge_request_diff.diff_refs_by_sha?
+ return Gitlab::Diff::DiffRefs.new(
+ base_sha: merge_request_diff.base_commit_sha,
+ start_sha: merge_request_diff.start_commit_sha,
+ head_sha: merge_request_diff.head_commit_sha
+ )
+ else
+ diff_refs
+ end
+ end
+
def validate_branches
if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target"
@@ -300,6 +322,8 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff.reload_content
+ MergeRequests::MergeRequestDiffCacheService.new.execute(self)
+
new_diff_refs = self.diff_refs
update_diff_notes_positions(
@@ -567,6 +591,14 @@ class MergeRequest < ActiveRecord::Base
!pipeline || pipeline.success?
end
+ def environments
+ return unless diff_head_commit
+
+ target_project.environments.select do |environment|
+ environment.includes_commit?(diff_head_commit)
+ end
+ end
+
def state_human_name
if merged?
"Merged"
@@ -670,7 +702,7 @@ class MergeRequest < ActiveRecord::Base
end
def support_new_diff_notes?
- diff_refs && diff_refs.complete?
+ diff_sha_refs && diff_sha_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 3f520c8f3ff..32cc6a3bfea 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -33,12 +33,12 @@ class MergeRequestDiff < ActiveRecord::Base
end
def size
- real_size.presence || diffs.size
+ real_size.presence || raw_diffs.size
end
- def diffs(options={})
+ def raw_diffs(options = {})
if options[:ignore_whitespace_change]
- @diffs_no_whitespace ||= begin
+ @raw_diffs_no_whitespace ||= begin
compare = Gitlab::Git::Compare.new(
repository.raw_repository,
self.start_commit_sha || self.target_branch_sha,
@@ -47,8 +47,8 @@ class MergeRequestDiff < ActiveRecord::Base
compare.diffs(options)
end
else
- @diffs ||= {}
- @diffs[options] ||= load_diffs(st_diffs, options)
+ @raw_diffs ||= {}
+ @raw_diffs[options] ||= load_diffs(st_diffs, options)
end
end
@@ -82,6 +82,10 @@ class MergeRequestDiff < ActiveRecord::Base
project.commit(self.head_commit_sha)
end
+ def diff_refs_by_sha?
+ base_commit_sha? && head_commit_sha? && start_commit_sha?
+ end
+
def compare
@compare ||=
begin
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 8b52cc824cd..7c29d27ce97 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,4 +1,6 @@
class Namespace < ActiveRecord::Base
+ acts_as_paranoid
+
include Sortable
include Gitlab::ShellAdapter
diff --git a/app/models/note.rb b/app/models/note.rb
index b6b2ac6aa42..ddcd7f9d034 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -5,6 +5,7 @@ class Note < ActiveRecord::Base
include Mentionable
include Awardable
include Importable
+ include FasterCacheKeys
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
diff --git a/app/models/project.rb b/app/models/project.rb
index 023b1dc3725..e0b28160937 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -379,9 +379,10 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC')
end
- # Deletes gitlab project export files older than 24 hours
- def remove_gitlab_exports!
- Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
+ def cached_count
+ Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
+ Project.count
+ end
end
end
@@ -451,7 +452,9 @@ class Project < ActiveRecord::Base
def add_import_job
if forked?
- job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
+ job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
+ forked_from_project.path_with_namespace,
+ self.namespace.path)
else
job_id = RepositoryImportWorker.perform_async(self.id)
end
@@ -584,7 +587,11 @@ class Project < ActiveRecord::Base
end
def to_param
- path
+ if persisted? && errors.include?(:path)
+ path_was
+ else
+ path
+ end
end
def to_reference(_from_project = nil)
@@ -599,6 +606,13 @@ class Project < ActiveRecord::Base
web_url.split('://')[1]
end
+ def new_issue_address(author)
+ if Gitlab::IncomingEmail.enabled? && author
+ Gitlab::IncomingEmail.reply_address(
+ "#{path_with_namespace}+#{author.authentication_token}")
+ end
+ end
+
def build_commit_note(commit)
notes.new(commit_id: commit.id, noteable_type: 'Commit')
end
@@ -857,16 +871,14 @@ class Project < ActiveRecord::Base
# Check if current branch name is marked as protected in the system
def protected_branch?(branch_name)
+ return true if empty_repo? && default_branch_protected?
+
@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.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)
+ def user_can_push_to_empty_repo?(user)
+ !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
def forked?
@@ -987,6 +999,10 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
+ def add_user(user, access_level, current_user = nil)
+ team.add_user(user, access_level, current_user)
+ end
+
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
@@ -1149,13 +1165,6 @@ class Project < ActiveRecord::Base
@wiki ||= ProjectWiki.new(self, self.owner)
end
- def schedule_delete!(user_id, params)
- # Queue this task for after the commit, so once we mark pending_delete it will run
- run_after_commit { ProjectDestroyWorker.perform_async(id, user_id, params) }
-
- update_attribute(:pending_delete, true)
- end
-
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
@@ -1245,8 +1254,23 @@ class Project < ActiveRecord::Base
authorized_for_user_by_shared_projects?(user, min_access_level)
end
+ def append_or_update_attribute(name, value)
+ old_values = public_send(name.to_s)
+
+ if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any?
+ update_attribute(name, old_values + value)
+ else
+ update_attribute(name, value)
+ end
+ end
+
private
+ def default_branch_protected?
+ current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
+ current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
+ end
+
def authorized_for_user_by_group?(user, min_access_level)
member = user.group_members.find_by(source_id: group)
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 511b2eac792..5af93860d09 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -1,4 +1,6 @@
class CampfireService < Service
+ include HTTParty
+
prop_accessor :token, :subdomain, :room
validates :token, presence: true, if: :activated?
@@ -29,18 +31,53 @@ class CampfireService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
- room = gate.find_room_by_name(self.room)
- return true unless room
-
+ self.class.base_uri base_uri
message = build_message(data)
-
- room.speak(message)
+ speak(self.room, message, auth)
end
private
- def gate
- @gate ||= Tinder::Campfire.new(subdomain, token: token)
+ def base_uri
+ @base_uri ||= "https://#{subdomain}.campfirenow.com"
+ end
+
+ def auth
+ # use a dummy password, as explained in the Campfire API doc:
+ # https://github.com/basecamp/campfire-api#authentication
+ @auth ||= {
+ basic_auth: {
+ username: token,
+ password: 'X'
+ }
+ }
+ end
+
+ # Post a message into a room, returns the message Hash in case of success.
+ # Returns nil otherwise.
+ # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
+ def speak(room_name, message, auth)
+ room = rooms(auth).find { |r| r["name"] == room_name }
+ return nil unless room
+
+ path = "/room/#{room["id"]}/speak.json"
+ body = {
+ body: {
+ message: {
+ type: 'TextMessage',
+ body: message
+ }
+ }
+ }
+ res = self.class.post(path, auth.merge(body))
+ res.code == 201 ? res : nil
+ end
+
+ # Returns a list of rooms, or [].
+ # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
+ def rooms(auth)
+ res = self.class.get("/rooms.json", auth)
+ res.code == 200 ? res["rooms"] : []
end
def build_message(push)
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 23e5b16221b..d7c986c1a91 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -46,7 +46,7 @@ class HipchatService < Service
return unless supported_events.include?(data[:object_kind])
message = create_message(data)
return unless message.present?
- gate[room].send('GitLab', message, message_options)
+ gate[room].send('GitLab', message, message_options(data))
end
def test(data)
@@ -67,8 +67,8 @@ class HipchatService < Service
@gate ||= HipChat::Client.new(token, options)
end
- def message_options
- { notify: notify.present? && notify == '1', color: color || 'yellow' }
+ def message_options(data = nil)
+ { notify: notify.present? && notify == '1', color: message_color(data) }
end
def create_message(data)
@@ -240,6 +240,21 @@ class HipchatService < Service
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
+ def message_color(data)
+ build_status_color(data) || color || 'yellow'
+ end
+
+ def build_status_color(data)
+ return unless data && data[:object_kind] == 'build'
+
+ case data[:commit][:status]
+ when 'success'
+ 'green'
+ else
+ 'red'
+ end
+ end
+
def project_name
project.name_with_namespace.gsub(/\s/, '')
end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index ad19b7795da..5301f9fa0ff 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,7 +1,9 @@
class PivotaltrackerService < Service
include HTTParty
- prop_accessor :token
+ API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+
+ prop_accessor :token, :restrict_to_branch
validates :token, presence: true, if: :activated?
def title
@@ -18,7 +20,17 @@ class PivotaltrackerService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ {
+ type: 'text',
+ name: 'token',
+ placeholder: 'Pivotal Tracker API token.'
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ placeholder: 'Comma-separated list of branches which will be ' \
+ 'automatically inspected. Leave blank to include all branches.'
+ }
]
end
@@ -28,8 +40,8 @@ class PivotaltrackerService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+ return unless allowed_branch?(data[:ref])
- url = 'https://www.pivotaltracker.com/services/v5/source_commits'
data[:commits].each do |commit|
message = {
'source_commit' => {
@@ -40,7 +52,7 @@ class PivotaltrackerService < Service
}
}
PivotaltrackerService.post(
- url,
+ API_ENDPOINT,
body: message.to_json,
headers: {
'Content-Type' => 'application/json',
@@ -49,4 +61,15 @@ class PivotaltrackerService < Service
)
end
end
+
+ private
+
+ def allowed_branch?(ref)
+ return true unless ref.present? && restrict_to_branch.present?
+
+ branch = Gitlab::Git.ref_name(ref)
+ allowed_branches = restrict_to_branch.split(',').map(&:strip)
+
+ branch.present? && allowed_branches.include?(branch)
+ end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 9d312a53790..d0a714cd6fc 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -34,7 +34,7 @@ class ProjectTeam
end
def add_users(users, access, current_user = nil)
- ProjectMember.add_users_into_projects(
+ ProjectMember.add_users_to_projects(
[project.id],
users,
access,
@@ -132,39 +132,68 @@ class ProjectTeam
Gitlab::Access.options_with_owner.key(max_member_access(user_id))
end
- # This method assumes project and group members are eager loaded for optimal
- # performance.
- def max_member_access(user_id)
- access = []
+ # Determine the maximum access level for a group of users in bulk.
+ #
+ # Returns a Hash mapping user ID -> maximum access level.
+ def max_member_access_for_user_ids(user_ids)
+ user_ids = user_ids.uniq
+ key = "max_member_access:#{project.id}"
- access += project.members.where(user_id: user_id).has_access.pluck(:access_level)
+ access = {}
- if group
- access += group.members.where(user_id: user_id).has_access.pluck(:access_level)
+ if RequestStore.active?
+ RequestStore.store[key] ||= {}
+ access = RequestStore.store[key]
end
- if project.invited_groups.any? && project.allowed_to_share_with_group?
- access << max_invited_level(user_id)
+ # Lookup only the IDs we need
+ user_ids = user_ids - access.keys
+
+ if user_ids.present?
+ user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
+
+ member_access = project.members.access_for_user_ids(user_ids)
+ merge_max!(access, member_access)
+
+ if group
+ group_access = group.members.access_for_user_ids(user_ids)
+ merge_max!(access, group_access)
+ end
+
+ # Each group produces a list of maximum access level per user. We take the
+ # max of the values produced by each group.
+ if project.invited_groups.any? && project.allowed_to_share_with_group?
+ project.project_group_links.each do |group_link|
+ invited_access = max_invited_level_for_users(group_link, user_ids)
+ merge_max!(access, invited_access)
+ end
+ end
end
- access.compact.max
+ access
+ end
+
+ def max_member_access(user_id)
+ max_member_access_for_user_ids([user_id])[user_id]
end
private
- def max_invited_level(user_id)
- project.project_group_links.map do |group_link|
- invited_group = group_link.group
- access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+ # For a given group, return the maximum access level for the user. This is the min of
+ # the invited access level of the group and the access level of the user within the group.
+ # For example, if the group has been given DEVELOPER access but the member has MASTER access,
+ # the user should receive only DEVELOPER access.
+ def max_invited_level_for_users(group_link, user_ids)
+ invited_group = group_link.group
+ capped_access_level = group_link.group_access
+ access = invited_group.group_members.access_for_user_ids(user_ids)
- # If group member has higher access level we should restrict it
- # to max allowed access level
- if access && access > group_link.group_access
- access = group_link.group_access
- end
+ # If the user is not in the list, assume he/she does not have access
+ missing_users = user_ids - access.keys
+ missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
- access
- end.compact.max
+ # Cap the maximum access by the invited level access
+ access.each { |key, value| access[key] = [value, capped_access_level].min }
end
def fetch_members(level = nil)
@@ -215,4 +244,8 @@ class ProjectTeam
def group
project.group
end
+
+ def merge_max!(first_hash, second_hash)
+ first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
+ end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index a255710f577..46f70da2452 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -56,6 +56,10 @@ class ProjectWiki
end
end
+ def repository_exists?
+ !!repository.exists?
+ end
+
def empty?
pages.empty?
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index b7011d7afdf..226b3f54342 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,6 +5,12 @@ class ProtectedBranch < ActiveRecord::Base
validates :name, presence: true
validates :project, presence: true
+ has_one :merge_access_level, dependent: :destroy
+ has_one :push_access_level, dependent: :destroy
+
+ accepts_nested_attributes_for :push_access_level
+ accepts_nested_attributes_for :merge_access_level
+
def commit
project.commit(self.name)
end
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
new file mode 100644
index 00000000000..b1112ee737d
--- /dev/null
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -0,0 +1,24 @@
+class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
+ belongs_to :protected_branch
+ delegate :project, to: :protected_branch
+
+ validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
+ Gitlab::Access::DEVELOPER] }
+
+ def self.human_access_levels
+ {
+ Gitlab::Access::MASTER => "Masters",
+ Gitlab::Access::DEVELOPER => "Developers + Masters"
+ }.with_indifferent_access
+ end
+
+ def check_access(user)
+ return true if user.is_admin?
+
+ project.team.max_member_access(user.id) >= access_level
+ end
+
+ def humanize
+ self.class.human_access_levels[self.access_level]
+ end
+end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
new file mode 100644
index 00000000000..6a5e49cf453
--- /dev/null
+++ b/app/models/protected_branch/push_access_level.rb
@@ -0,0 +1,27 @@
+class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
+ belongs_to :protected_branch
+ delegate :project, to: :protected_branch
+
+ validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS] }
+
+ def self.human_access_levels
+ {
+ Gitlab::Access::MASTER => "Masters",
+ Gitlab::Access::DEVELOPER => "Developers + Masters",
+ Gitlab::Access::NO_ACCESS => "No one"
+ }.with_indifferent_access
+ end
+
+ def check_access(user)
+ return false if access_level == Gitlab::Access::NO_ACCESS
+ return true if user.is_admin?
+
+ project.team.max_member_access(user.id) >= access_level
+ end
+
+ def humanize
+ self.class.human_access_levels[self.access_level]
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e9d5f4c91f8..e56bac509a4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -70,7 +70,12 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
- commit = Gitlab::Git::Commit.find(raw_repository, ref)
+ commit =
+ if ref.is_a?(Gitlab::Git::Commit)
+ ref
+ else
+ Gitlab::Git::Commit.find(raw_repository, ref)
+ end
commit = ::Commit.new(commit, @project) if commit
commit
rescue Rugged::OdbError
@@ -158,7 +163,7 @@ class Repository
before_remove_branch
branch = find_branch(branch_name)
- oldrev = branch.try(:target)
+ oldrev = branch.try(:target).try(:id)
newrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
@@ -211,6 +216,9 @@ class Repository
rugged.references.create(keep_around_ref_name(sha), sha, force: true)
rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ rescue Rugged::OSError => ex
+ raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
+ Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
end
end
@@ -256,10 +264,10 @@ class Repository
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind = raw_repository.
- count_commits_between(branch.target, root_ref_hash)
+ count_commits_between(branch.target.sha, root_ref_hash)
number_commits_ahead = raw_repository.
- count_commits_between(root_ref_hash, branch.target)
+ count_commits_between(root_ref_hash, branch.target.sha)
{ behind: number_commits_behind, ahead: number_commits_ahead }
end
@@ -364,7 +372,7 @@ class Repository
# We don't want to flush the cache if the commit didn't actually make any
# changes to any of the possible avatar files.
if revision && commit = self.commit(revision)
- return unless commit.diffs.
+ return unless commit.raw_diffs(deltas_only: true).
any? { |diff| AVATAR_FILES.include?(diff.new_path) }
end
@@ -593,7 +601,7 @@ class Repository
commit(sha)
end
- def next_branch(name, opts={})
+ def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
result = n.match(/\A#{name}-([0-9]+)\z/)
@@ -612,11 +620,11 @@ class Repository
case value
when 'name'
branches.sort_by(&:name)
- when 'recently_updated'
+ when 'updated_desc'
branches.sort do |a, b|
commit(b.target).committed_date <=> commit(a.target).committed_date
end
- when 'last_updated'
+ when 'updated_asc'
branches.sort do |a, b|
commit(a.target).committed_date <=> commit(b.target).committed_date
end
@@ -628,9 +636,7 @@ class Repository
def tags_sorted_by(value)
case value
when 'name'
- # Would be better to use `sort_by` but `version_sorter` only exposes
- # `sort` and `rsort`
- VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) }
+ VersionSorter.rsort(tags) { |tag| tag.name }
when 'updated_desc'
tags_sorted_by_committed_date.reverse
when 'updated_asc'
@@ -685,9 +691,7 @@ class Repository
end
def local_branches
- @local_branches ||= rugged.branches.each(:local).map do |branch|
- Gitlab::Git::Branch.new(branch.name, branch.target)
- end
+ @local_branches ||= raw_repository.local_branches
end
alias_method :branches, :local_branches
@@ -828,7 +832,7 @@ class Repository
end
def revert(user, commit, base_branch, revert_tree_id = nil)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
revert_tree_id ||= check_revert_content(commit, base_branch)
return false unless revert_tree_id
@@ -845,7 +849,7 @@ class Repository
end
def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
return false unless cherry_pick_tree_id
@@ -866,7 +870,7 @@ class Repository
end
def check_revert_content(commit, base_branch)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
@@ -880,7 +884,7 @@ class Repository
end
def check_cherry_pick_content(commit, base_branch)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha]
args << 1 if commit.merge_commit?
@@ -971,7 +975,7 @@ class Repository
was_empty = empty?
if !was_empty && target_branch
- oldrev = target_branch.target
+ oldrev = target_branch.target.id
end
# Make commit
@@ -985,9 +989,13 @@ class Repository
if was_empty || !target_branch
# Create branch
rugged.references.create(ref, newrev)
+
+ # If repo was empty expire cache
+ after_create if was_empty
+ after_create_branch
else
# Update head
- current_head = find_branch(branch).target
+ current_head = find_branch(branch).target.id
# Make sure target branch was not changed during pre-receive hook
if current_head == oldrev
@@ -1045,7 +1053,7 @@ class Repository
end
def tags_sorted_by_committed_date
- tags.sort_by { |tag| commit(tag.target).committed_date }
+ tags.sort_by { |tag| tag.target.committed_date }
end
def keep_around_ref_name(sha)
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 12df68ef83b..3b8b9833565 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
user.block
user.destroy
end
+
+ def text
+ [title, description].join("\n")
+ end
end
diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb
deleted file mode 100644
index cdc7321b08e..00000000000
--- a/app/models/spam_report.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class SpamReport < ActiveRecord::Base
- belongs_to :user
-
- validates :user, presence: true
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index db747434959..87a2d999843 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,13 +23,13 @@ class User < ActiveRecord::Base
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
- key: Gitlab::Application.config.secret_key_base,
+ key: Gitlab::Application.secrets.otp_key_base,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
algorithm: 'aes-256-cbc'
devise :two_factor_authenticatable,
- otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
+ otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON
@@ -809,13 +809,13 @@ class User < ActiveRecord::Base
def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
- todos.done.count
+ TodosFinder.new(self, state: :done).execute.count
end
end
def todos_pending_count(force: false)
Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
- todos.pending.count
+ TodosFinder.new(self, state: :pending).execute.count
end
end
diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb
new file mode 100644
index 00000000000..0949c6ef083
--- /dev/null
+++ b/app/models/user_agent_detail.rb
@@ -0,0 +1,9 @@
+class UserAgentDetail < ActiveRecord::Base
+ belongs_to :subject, polymorphic: true
+
+ validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
+
+ def submittable?
+ !submitted?
+ end
+end