summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorTimothy Andrew <mail@timothyandrew.net>2016-09-07 14:58:01 +0530
committerTimothy Andrew <mail@timothyandrew.net>2016-09-07 14:58:01 +0530
commitf5b9837c19f48255237eca1e618fc632a45b58b5 (patch)
tree2653e9aa1e3bf0a1db6630bb52863c30e6313d87 /app/models
parent72b9b87ca93615640dcc022f324b2af16ceaa4eb (diff)
parent1d5488699678d22644d24add4b89cede0419ad25 (diff)
downloadgitlab-ce-f5b9837c19f48255237eca1e618fc632a45b58b5.tar.gz
Merge remote-tracking branch 'origin/master' into 21170-cycle-analytics
Diffstat (limited to 'app/models')
-rw-r--r--app/models/ability.rb588
-rw-r--r--app/models/ci/build.rb27
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/commit.rb9
-rw-r--r--app/models/commit_range.rb7
-rw-r--r--app/models/concerns/awardable.rb14
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/note_on_diff.rb4
-rw-r--r--app/models/concerns/project_features_compatibility.rb37
-rw-r--r--app/models/concerns/taskable.rb4
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb1
-rw-r--r--app/models/merge_request.rb43
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb51
-rw-r--r--app/models/project_feature.rb63
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb2
-rw-r--r--app/models/repository.rb58
-rw-r--r--app/models/service.rb4
-rw-r--r--app/models/user.rb8
23 files changed, 269 insertions, 672 deletions
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 5293be33e5a..fa8f8bc3a5f 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,34 +1,5 @@
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)
- 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)
- when Issue then issue_abilities(user, subject)
- when Note then note_abilities(user, subject)
- when ProjectSnippet then project_snippet_abilities(user, subject)
- when PersonalSnippet then personal_snippet_abilities(user, subject)
- when MergeRequest then merge_request_abilities(user, subject)
- when Group then group_abilities(user, subject)
- when Namespace then namespace_abilities(user, subject)
- when GroupMember then group_member_abilities(user, subject)
- 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
-
# Given a list of users and a project this method returns the users that can
# read the given project.
def users_that_can_read_project(users, project)
@@ -61,360 +32,7 @@ class Ability
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)
- anonymous_personal_snippet_abilities(subject)
- elsif subject.is_a?(ProjectSnippet)
- anonymous_project_snippet_abilities(subject)
- elsif subject.is_a?(CommitStatus)
- anonymous_commit_status_abilities(subject)
- elsif subject.is_a?(Project) || subject.respond_to?(:project)
- anonymous_project_abilities(subject)
- elsif subject.is_a?(Group) || subject.respond_to?(:group)
- anonymous_group_abilities(subject)
- elsif subject.is_a?(User)
- anonymous_user_abilities
- else
- []
- end
- end
-
- def anonymous_project_abilities(subject)
- project = if subject.is_a?(Project)
- subject
- else
- subject.project
- end
-
- if project && project.public?
- rules = [
- :read_project,
- :read_board,
- :read_list,
- :read_wiki,
- :read_label,
- :read_milestone,
- :read_project_snippet,
- :read_project_member,
- :read_merge_request,
- :read_note,
- :read_pipeline,
- :read_commit_status,
- :read_container_image,
- :download_code
- ]
-
- # Allow to read builds by anonymous user if guests are allowed
- rules << :read_build if project.public_builds?
-
- # Allow to read issues by anonymous user if issue is not confidential
- rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
-
- rules - project_disabled_features_rules(project)
- else
- []
- end
- end
-
- def anonymous_commit_status_abilities(subject)
- rules = anonymous_project_abilities(subject.project)
- # If subject is Ci::Build which inherits from CommitStatus filter the abilities
- rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
- rules
- end
-
- def anonymous_group_abilities(subject)
- rules = []
-
- group = if subject.is_a?(Group)
- subject
- else
- subject.group
- end
-
- rules << :read_group if group.public?
-
- rules
- end
-
- def anonymous_personal_snippet_abilities(snippet)
- if snippet.public?
- [:read_personal_snippet]
- else
- []
- end
- end
-
- def anonymous_project_snippet_abilities(snippet)
- if snippet.public?
- [:read_project_snippet]
- else
- []
- end
- end
-
- def anonymous_user_abilities
- [:read_user] unless restricted_public_level?
- end
-
- def global_abilities(user)
- rules = []
- rules << :create_group if user.can_create_group
- rules << :read_users_list
- rules
- end
-
- def project_abilities(user, project)
- key = "/user/#{user.id}/project/#{project.id}"
-
- if RequestStore.active?
- RequestStore.store[key] ||= uncached_project_abilities(user, project)
- else
- uncached_project_abilities(user, project)
- end
- end
-
- def uncached_project_abilities(user, project)
- rules = []
- # Push abilities on the users team role
- rules.push(*project_team_rules(project.team, user))
-
- owner = user.admin? ||
- project.owner == user ||
- (project.group && project.group.has_owner?(user))
-
- if owner
- rules.push(*project_owner_rules)
- end
-
- if project.public? || (project.internal? && !user.external?)
- rules.push(*public_project_rules)
-
- # 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 if project.request_access_enabled
- end
- end
-
- if project.archived?
- rules -= project_archived_rules
- end
-
- (rules - project_disabled_features_rules(project)).uniq
- end
-
- def project_team_rules(team, user)
- # Rules based on role in project
- if team.master?(user)
- project_master_rules
- elsif team.developer?(user)
- project_dev_rules
- elsif team.reporter?(user)
- project_report_rules
- elsif team.guest?(user)
- project_guest_rules
- else
- []
- end
- end
-
- def public_project_rules
- @public_project_rules ||= project_guest_rules + [
- :download_code,
- :fork_project,
- :read_commit_status,
- :read_pipeline,
- :read_container_image
- ]
- end
-
- def project_guest_rules
- @project_guest_rules ||= [
- :read_project,
- :read_wiki,
- :read_issue,
- :read_board,
- :read_list,
- :read_label,
- :read_milestone,
- :read_project_snippet,
- :read_project_member,
- :read_merge_request,
- :read_note,
- :create_project,
- :create_issue,
- :create_note,
- :upload_file
- ]
- end
-
- def project_report_rules
- @project_report_rules ||= project_guest_rules + [
- :download_code,
- :fork_project,
- :create_project_snippet,
- :update_issue,
- :admin_issue,
- :admin_label,
- :admin_list,
- :read_commit_status,
- :read_build,
- :read_container_image,
- :read_pipeline,
- :read_environment,
- :read_deployment,
- :read_cycle_analytics
- ]
- end
-
- def project_dev_rules
- @project_dev_rules ||= project_report_rules + [
- :admin_merge_request,
- :update_merge_request,
- :create_commit_status,
- :update_commit_status,
- :create_build,
- :update_build,
- :create_pipeline,
- :update_pipeline,
- :create_merge_request,
- :create_wiki,
- :push_code,
- :resolve_note,
- :create_container_image,
- :update_container_image,
- :create_environment,
- :create_deployment
- ]
- end
-
- def project_archived_rules
- @project_archived_rules ||= [
- :create_merge_request,
- :push_code,
- :push_code_to_protected_branches,
- :update_merge_request,
- :admin_merge_request
- ]
- end
-
- def project_master_rules
- @project_master_rules ||= project_dev_rules + [
- :push_code_to_protected_branches,
- :update_project_snippet,
- :update_environment,
- :update_deployment,
- :admin_milestone,
- :admin_project_snippet,
- :admin_project_member,
- :admin_merge_request,
- :admin_note,
- :admin_wiki,
- :admin_project,
- :admin_commit_status,
- :admin_build,
- :admin_container_image,
- :admin_pipeline,
- :admin_environment,
- :admin_deployment
- ]
- end
-
- def project_owner_rules
- @project_owner_rules ||= project_master_rules + [
- :change_namespace,
- :change_visibility_level,
- :rename_project,
- :remove_project,
- :archive_project,
- :remove_fork_project,
- :destroy_merge_request,
- :destroy_issue
- ]
- end
-
- def project_disabled_features_rules(project)
- rules = []
-
- unless project.issues_enabled
- rules += named_abilities('issue')
- end
-
- unless project.merge_requests_enabled
- rules += named_abilities('merge_request')
- end
-
- unless project.issues_enabled or project.merge_requests_enabled
- rules += named_abilities('label')
- rules += named_abilities('milestone')
- end
-
- unless project.snippets_enabled
- rules += named_abilities('project_snippet')
- end
-
- unless project.wiki_enabled
- rules += named_abilities('wiki')
- end
-
- unless project.builds_enabled
- rules += named_abilities('build')
- rules += named_abilities('pipeline')
- rules += named_abilities('environment')
- rules += named_abilities('deployment')
- end
-
- unless project.container_registry_enabled
- rules += named_abilities('container_image')
- end
-
- rules
- end
-
- def group_abilities(user, group)
- 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 master
- rules += [
- :create_projects,
- :admin_milestones
- ]
- end
-
- # Only group owner and administrators can admin group
- if owner
- rules += [
- :admin_group,
- :admin_namespace,
- :admin_group_member,
- :change_visibility_level
- ]
- end
-
- if group.public? || (group.internal? && !user.external?)
- rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
- end
-
- rules.flatten
- end
-
- def can_read_group?(user, group)
- return true if user.admin?
- return true if group.public?
- return true if group.internal? && !user.external?
- return true if group.users.include?(user)
-
- GroupProjectsFinder.new(group).execute(user).any?
- end
-
+ # TODO: make this private and use the actual abilities stuff for this
def can_edit_note?(user, note)
return false if !note.editable? || !user.present?
return true if note.author == user || user.admin?
@@ -427,207 +45,23 @@ class Ability
end
end
- def namespace_abilities(user, namespace)
- rules = []
-
- # Only namespace owner and administrators can admin it
- if namespace.owner == user || user.admin?
- rules += [
- :create_projects,
- :admin_namespace
- ]
- end
-
- rules.flatten
- end
-
- [:issue, :merge_request].each do |name|
- define_method "#{name}_abilities" do |user, subject|
- rules = []
-
- if subject.author == user || (subject.respond_to?(:assignee) && subject.assignee == user)
- rules += [
- :"read_#{name}",
- :"update_#{name}",
- ]
- end
-
- rules += project_abilities(user, subject.project)
- rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
- rules
- end
- end
-
- def note_abilities(user, note)
- rules = []
-
- if note.author == user
- rules += [
- :read_note,
- :update_note,
- :admin_note,
- :resolve_note
- ]
- end
-
- if note.respond_to?(:project) && note.project
- rules += project_abilities(user, note.project)
- end
-
- if note.for_merge_request? && note.noteable.author == user
- rules << :resolve_note
- end
-
- rules
- end
-
- def personal_snippet_abilities(user, snippet)
- rules = []
-
- if snippet.author == user
- rules += [
- :read_personal_snippet,
- :update_personal_snippet,
- :admin_personal_snippet
- ]
- end
-
- if snippet.public? || (snippet.internal? && !user.external?)
- rules << :read_personal_snippet
- end
-
- rules
+ def allowed?(user, action, subject)
+ allowed(user, subject).include?(action)
end
- def project_snippet_abilities(user, snippet)
- rules = []
-
- if snippet.author == user || user.admin?
- rules += [
- :read_project_snippet,
- :update_project_snippet,
- :admin_project_snippet
- ]
- end
-
- if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user))
- rules << :read_project_snippet
- end
-
- rules
- end
-
- def group_member_abilities(user, subject)
- rules = []
- target_user = subject.user
- group = subject.group
-
- unless group.last_owner?(target_user)
- can_manage = group_abilities(user, group).include?(:admin_group_member)
-
- if can_manage
- rules << :update_group_member
- rules << :destroy_group_member
- elsif user == target_user
- rules << :destroy_group_member
- end
- end
-
- rules
- end
-
- def project_member_abilities(user, subject)
- rules = []
- target_user = subject.user
- project = subject.project
-
- unless target_user == project.owner
- can_manage = project_abilities(user, project).include?(:admin_project_member)
-
- if can_manage
- rules << :update_project_member
- rules << :destroy_project_member
- elsif user == target_user
- rules << :destroy_project_member
- end
- end
-
- rules
- end
-
- def commit_status_abilities(user, subject)
- rules = project_abilities(user, subject.project)
- # If subject is Ci::Build which inherits from CommitStatus filter the abilities
- rules = filter_build_abilities(rules) if subject.is_a?(Ci::Build)
- rules
- end
-
- def filter_build_abilities(rules)
- # If we can't read build we should also not have that
- # ability when looking at this in context of commit_status
- %w(read create update admin).each do |rule|
- rules.delete(:"#{rule}_commit_status") unless rules.include?(:"#{rule}_build")
- end
- 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 allowed(user, subject)
+ return uncached_allowed(user, subject) unless RequestStore.active?
- def user_abilities
- [:read_user]
- end
-
- def abilities
- @abilities ||= begin
- abilities = Six.new
- abilities << self
- abilities
- end
+ user_key = user ? user.id : 'anonymous'
+ subject_key = subject ? "#{subject.class.name}/#{subject.id}" : 'global'
+ key = "/ability/#{user_key}/#{subject_key}"
+ RequestStore[key] ||= uncached_allowed(user, subject).freeze
end
private
- def restricted_public_level?
- current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
- end
-
- def named_abilities(name)
- [
- :"read_#{name}",
- :"create_#{name}",
- :"update_#{name}",
- :"admin_#{name}"
- ]
- end
-
- def filter_confidential_issues_abilities(user, issue, rules)
- return rules if user.admin? || !issue.confidential?
-
- unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
- rules.delete(:admin_issue)
- rules.delete(:read_issue)
- rules.delete(:update_issue)
- end
-
- 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)
- )
+ def uncached_allowed(user, subject)
+ BasePolicy.class_for(subject).abilities(user, subject)
end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 23c8de6f650..61052437318 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -208,22 +208,31 @@ module Ci
end
end
+ def has_trace_file?
+ File.exist?(path_to_trace) || has_old_trace_file?
+ end
+
def has_trace?
raw_trace.present?
end
def raw_trace
- if File.file?(path_to_trace)
- File.read(path_to_trace)
- elsif project.ci_id && File.file?(old_path_to_trace)
- # Temporary fix for build trace data integrity
- File.read(old_path_to_trace)
+ if File.exist?(trace_file_path)
+ File.read(trace_file_path)
else
# backward compatibility
read_attribute :trace
end
end
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ def has_old_trace_file?
+ project.ci_id && File.exist?(old_path_to_trace)
+ end
+
def trace
trace = raw_trace
if project && trace.present? && project.runners_token.present?
@@ -262,6 +271,14 @@ module Ci
end
end
+ def trace_file_path
+ if has_old_trace_file?
+ old_path_to_trace
+ else
+ path_to_trace
+ end
+ end
+
def dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 03812cd195f..bd1737c7587 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -65,8 +65,8 @@ module Ci
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)
+ def self.latest_successful_for(ref)
+ where(ref: ref).order(id: :desc).success.first
end
def self.truncate_sha(sha)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 817d063e4a2..e64fd1e0c1b 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -108,15 +108,6 @@ class Commit
@diff_line_count
end
- # Returns a string describing the commit for use in a link title
- #
- # Example
- #
- # "Commit: Alex Denisov - Project git clone panel"
- def link_title
- "Commit: #{author_name} - #{title}"
- end
-
# Returns the commits title.
#
# Usually, the commit title is the first line of the commit message.
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 630ee9601e0..656a242c265 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -4,12 +4,10 @@
#
# range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false
-# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013"
#
# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true
-# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013"
#
@@ -109,11 +107,6 @@ class CommitRange
reference
end
- # Returns a String for use in a link's title attribute
- def reference_title
- "Commits #{sha_start} through #{sha_to}"
- end
-
# Return a Hash of parameters for passing to a URL helper
#
# See `namespace_project_compare_url`
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 800a16ab246..d8d4575bb4d 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -2,7 +2,7 @@ module Awardable
extend ActiveSupport::Concern
included do
- has_many :award_emoji, -> { includes(:user) }, as: :awardable, dependent: :destroy
+ has_many :award_emoji, -> { includes(:user).order(:id) }, as: :awardable, dependent: :destroy
if self < Participable
# By default we always load award_emoji user association
@@ -59,6 +59,18 @@ module Awardable
true
end
+ def awardable_votes?(name)
+ AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name
+ end
+
+ def user_can_award?(current_user, name)
+ if user_authored?(current_user)
+ !awardable_votes?(normalize_name(name))
+ else
+ true
+ end
+ end
+
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 8e11d4f57cf..22231b2e0f0 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -196,6 +196,10 @@ module Issuable
end
end
+ def user_authored?(user)
+ user == author
+ end
+
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index a881fb83b7f..b8dd27a7afe 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -28,4 +28,8 @@ module NoteOnDiff
def can_be_award_emoji?
false
end
+
+ def to_discussion
+ Discussion.new([self])
+ end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
new file mode 100644
index 00000000000..9216122923e
--- /dev/null
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -0,0 +1,37 @@
+# Makes api V3 compatible with old project features permissions methods
+#
+# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
+# fields to a new table "project_features", support for the old fields is still needed in the API.
+
+module ProjectFeaturesCompatibility
+ extend ActiveSupport::Concern
+
+ def wiki_enabled=(value)
+ write_feature_attribute(:wiki_access_level, value)
+ end
+
+ def builds_enabled=(value)
+ write_feature_attribute(:builds_access_level, value)
+ end
+
+ def merge_requests_enabled=(value)
+ write_feature_attribute(:merge_requests_access_level, value)
+ end
+
+ def issues_enabled=(value)
+ write_feature_attribute(:issues_access_level, value)
+ end
+
+ def snippets_enabled=(value)
+ write_feature_attribute(:snippets_access_level, value)
+ end
+
+ private
+
+ def write_feature_attribute(field, value)
+ build_project_feature unless project_feature
+
+ access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
+ project_feature.update_attribute(field, access_level)
+ end
+end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index df2a9e3e84b..a3ac577cf3e 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -52,11 +52,11 @@ module Taskable
end
# Return a string that describes the current state of this Taskable's task
- # list items, e.g. "20 tasks (12 completed, 8 remaining)"
+ # list items, e.g. "12 of 20 tasks completed"
def task_status
return '' if description.blank?
sum = tasks.summary
- "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
+ "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c8320ff87fa..4442cefc7e9 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -107,10 +107,6 @@ class DiffNote < Note
self.noteable.find_diff_discussion(self.discussion_id)
end
- def to_discussion
- Discussion.new([self])
- end
-
private
def supported?
diff --git a/app/models/event.rb b/app/models/event.rb
index fd736d12359..a0b7b0dc2b5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -65,7 +65,7 @@ class Event < ActiveRecord::Base
elsif created_project?
true
elsif issue? || issue_note?
- Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
+ Ability.allowed?(user, :read_issue, note? ? note_target : target)
else
((merge_request? || note?) && target.present?) || milestone?
end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 836a75b0608..c631e7a7df5 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@ class ProjectHook < WebHook
belongs_to :project
scope :issue_hooks, -> { where(issues_events: true) }
+ scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f365dee3141..595602e80fe 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, false
+ default_value_for :confidential_issues_events, false
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 99a9f83cd50..8127c2cdd8d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -94,13 +94,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- validates :source_project, presence: true, unless: [:allow_broken, :importing?]
+ validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
- validate :validate_branches, unless: [:allow_broken, :importing?]
- validate :validate_fork
+ validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
+ validate :validate_fork, unless: :closed_without_fork?
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
@@ -243,12 +243,12 @@ class MergeRequest < ActiveRecord::Base
def source_branch_head
source_branch_ref = @source_branch_sha || source_branch
- source_project.repository.commit(source_branch) if source_branch_ref
+ source_project.repository.commit(source_branch_ref) 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
+ target_project.repository.commit(target_branch_ref) if target_branch_ref
end
def branch_merge_base_commit
@@ -308,19 +308,22 @@ class MergeRequest < ActiveRecord::Base
def validate_fork
return true unless target_project && source_project
+ return true if target_project == source_project
+ return true unless forked_source_project_missing?
- if target_project == source_project
- true
- else
- # If source and target projects are different
- # we should check if source project is actually a fork of target project
- if source_project.forked_from?(target_project)
- true
- else
- errors.add :validate_fork,
- 'Source project is not a fork of target project'
- end
- end
+ errors.add :validate_fork,
+ 'Source project is not a fork of the target project'
+ end
+
+ def closed_without_fork?
+ closed? && forked_source_project_missing?
+ end
+
+ def forked_source_project_missing?
+ return false unless for_fork?
+ return true unless source_project
+
+ !source_project.forked_from?(target_project)
end
def ensure_merge_request_diff
@@ -411,7 +414,7 @@ class MergeRequest < ActiveRecord::Base
def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
- Ability.abilities.allowed?(current_user, :push_code, source_project) &&
+ Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
end
@@ -729,7 +732,9 @@ class MergeRequest < ActiveRecord::Base
end
def pipeline
- @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
+ return unless diff_head_sha && source_project
+
+ @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines
diff --git a/app/models/note.rb b/app/models/note.rb
index f2656df028b..b94e3cff2ce 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -223,6 +223,10 @@ class Note < ActiveRecord::Base
end
end
+ def user_authored?(user)
+ user == author
+ end
+
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0e4fb94f8eb..a6de2c48071 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -11,24 +11,23 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
+ include ProjectFeaturesCompatibility
extend Gitlab::ConfigHelper
UNKNOWN_IMPORT_URL = 'http://unknown.git'
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
- default_value_for :issues_enabled, gitlab_config_features.issues
- default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
- default_value_for :builds_enabled, gitlab_config_features.builds
- 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?
+ after_initialize :setup_project_feature
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -62,10 +61,10 @@ class Project < ActiveRecord::Base
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
- has_one :board, dependent: :destroy
-
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
+ has_one :board, dependent: :destroy
+
# Project services
has_many :services
has_one :campfire_service, dependent: :destroy
@@ -130,6 +129,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
+ has_one :project_feature, dependent: :destroy
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
@@ -142,6 +142,7 @@ class Project < ActiveRecord::Base
has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
+ accepts_nested_attributes_for :project_feature
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -159,8 +160,6 @@ class Project < ActiveRecord::Base
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
- validates :issues_enabled, :merge_requests_enabled,
- :wiki_enabled, inclusion: { in: [true, false] }
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
@@ -196,6 +195,9 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
+ scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
+ scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
+
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
@@ -390,6 +392,13 @@ class Project < ActiveRecord::Base
end
end
+ def lfs_enabled?
+ return false unless Gitlab.config.lfs.enabled
+ return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+
+ self[:lfs_enabled]
+ end
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]
end
@@ -436,7 +445,7 @@ class Project < ActiveRecord::Base
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
- latest_pipeline = pipelines.latest_successful_for(ref).first
+ latest_pipeline = pipelines.latest_successful_for(ref)
if latest_pipeline
latest_pipeline.builds.latest.with_artifacts
@@ -680,6 +689,10 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
+ def has_wiki?
+ wiki_enabled? || has_external_wiki?
+ end
+
def external_wiki
if has_external_wiki.nil?
cache_has_external_wiki # Populate
@@ -1096,16 +1109,21 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
end
- def pipeline(sha, ref)
+ def pipeline_for(ref, sha = nil)
+ sha ||= commit(ref).try(:sha)
+
+ return unless sha
+
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
- def ensure_pipeline(sha, ref, current_user = nil)
- pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
+ def ensure_pipeline(ref, sha, current_user = nil)
+ pipeline_for(ref, sha) ||
+ pipelines.create(sha: sha, ref: ref, user: current_user)
end
def enable_ci
- self.builds_enabled = true
+ project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
def any_runners?(&block)
@@ -1272,6 +1290,11 @@ class Project < ActiveRecord::Base
private
+ # Prevents the creation of project_feature record for every project
+ def setup_project_feature
+ build_project_feature unless project_feature
+ end
+
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
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
new file mode 100644
index 00000000000..9c602c582bd
--- /dev/null
+++ b/app/models/project_feature.rb
@@ -0,0 +1,63 @@
+class ProjectFeature < ActiveRecord::Base
+ # == Project features permissions
+ #
+ # Grants access level to project tools
+ #
+ # Tools can be enabled only for users, everyone or disabled
+ # Access control is made only for non private projects
+ #
+ # levels:
+ #
+ # Disabled: not enabled for anyone
+ # Private: enabled only for team members
+ # Enabled: enabled for everyone able to access the project
+ #
+
+ # Permision levels
+ DISABLED = 0
+ PRIVATE = 10
+ ENABLED = 20
+
+ FEATURES = %i(issues merge_requests wiki snippets builds)
+
+ belongs_to :project
+
+ def feature_available?(feature, user)
+ raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
+
+ get_permission(user, public_send("#{feature}_access_level"))
+ end
+
+ def builds_enabled?
+ return true unless builds_access_level
+
+ builds_access_level > DISABLED
+ end
+
+ def wiki_enabled?
+ return true unless wiki_access_level
+
+ wiki_access_level > DISABLED
+ end
+
+ def merge_requests_enabled?
+ return true unless merge_requests_access_level
+
+ merge_requests_access_level > DISABLED
+ end
+
+ private
+
+ def get_permission(user, level)
+ case level
+ when DISABLED
+ false
+ when PRIVATE
+ user && (project.team.member?(user) || user.admin?)
+ when ENABLED
+ true
+ else
+ true
+ end
+ end
+end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index d7c986c1a91..afebd3b6a12 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -39,7 +39,7 @@ class HipchatService < Service
end
def supported_events
- %w(push issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push build)
end
def execute(data)
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index abbc780dc1a..e6c943db2bf 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -44,7 +44,7 @@ class SlackService < Service
end
def supported_events
- %w(push issue merge_request note tag_push build wiki_page)
+ %w(push issue confidential_issue merge_request note tag_push build wiki_page)
end
def execute(data)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 91bdafdac99..414b82516bc 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -120,8 +120,21 @@ class Repository
commits
end
- def find_branch(name)
- raw_repository.branches.find { |branch| branch.name == name }
+ def find_branch(name, fresh_repo: true)
+ # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may
+ # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate
+ # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc)
+ # may cause the branch to "disappear" erroneously or have the wrong SHA.
+ #
+ # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
+ raw_repo =
+ if fresh_repo
+ Gitlab::Git::Repository.new(path_to_repo)
+ else
+ raw_repository
+ end
+
+ raw_repo.find_branch(name)
end
def find_tag(name)
@@ -136,7 +149,7 @@ class Repository
return false unless target
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.branches.create(branch_name, target)
+ update_ref!(ref, target, oldrev)
end
after_create_branch
@@ -168,7 +181,7 @@ class Repository
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
- rugged.branches.delete(branch_name)
+ update_ref!(ref, newrev, oldrev)
end
after_remove_branch
@@ -202,6 +215,21 @@ class Repository
rugged.references.exist?(ref)
end
+ def update_ref!(name, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # offer 'compare and swap' ref updates. Without compare-and-swap we can
+ # (and have!) accidentally reset the ref to an earlier state, clobbering
+ # commits. See also https://github.com/libgit2/libgit2/issues/1534.
+ command = %w[git update-ref --stdin -z]
+ _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
+ stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ return if status.zero?
+
+ raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
+ 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
@@ -1001,15 +1029,10 @@ class Repository
def commit_with_hooks(current_user, branch)
update_autocrlf_option
- oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
target_branch = find_branch(branch)
was_empty = empty?
- if !was_empty && target_branch
- oldrev = target_branch.target.id
- end
-
# Make commit
newrev = yield(ref)
@@ -1017,24 +1040,15 @@ class Repository
raise CommitError.new('Failed to create commit')
end
+ oldrev = rugged.lookup(newrev).parent_ids.first || Gitlab::Git::BLANK_SHA
+
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
+ update_ref!(ref, newrev, oldrev)
+
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.id
-
- # 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
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 09b4717a523..198e7247838 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -7,6 +7,7 @@ class Service < ActiveRecord::Base
default_value_for :active, false
default_value_for :push_events, true
default_value_for :issues_events, true
+ default_value_for :confidential_issues_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
@@ -33,6 +34,7 @@ class Service < ActiveRecord::Base
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
+ scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
@@ -100,7 +102,7 @@ class Service < ActiveRecord::Base
end
def supported_events
- %w(push tag_push issue merge_request wiki_page)
+ %w(push tag_push issue confidential_issue merge_request wiki_page)
end
def execute(data)
diff --git a/app/models/user.rb b/app/models/user.rb
index ad3cfbc03e4..6996740eebd 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -433,7 +433,7 @@ class User < ActiveRecord::Base
#
# This logic is duplicated from `Ability#project_abilities` into a SQL form.
def projects_where_can_admin_issues
- authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
+ authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled
end
def is_admin?
@@ -460,16 +460,12 @@ class User < ActiveRecord::Base
can?(:create_group, nil)
end
- def abilities
- Ability.abilities
- end
-
def can_select_namespace?
several_namespaces? || admin
end
def can?(action, subject)
- abilities.allowed?(self, action, subject)
+ Ability.allowed?(self, action, subject)
end
def first_name