summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2016-11-24 11:32:59 +0000
committerPhil Hughes <me@iamphill.com>2016-11-24 11:32:59 +0000
commit8c4f4afd6dd6d382aab2d6b992b6ffe3e60f91af (patch)
tree37d3ff76dc31e7fcfa63eb8c2f54c9d84eb9b88a /app/models
parent03a235783f697572fe201332cb82746401a01daf (diff)
parent3e44ed3e2bf75bb14a2d8b0466b3d92afd0ea067 (diff)
downloadgitlab-ce-autocomplete-space-prefix.tar.gz
Merge branch 'master' into autocomplete-space-prefixautocomplete-space-prefix
Diffstat (limited to 'app/models')
-rw-r--r--app/models/application_setting.rb21
-rw-r--r--app/models/chat_name.rb12
-rw-r--r--app/models/ci/build.rb45
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/issuable.rb13
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb24
-rw-r--r--app/models/concerns/select_for_project_authorization.rb9
-rw-r--r--app/models/concerns/subscribable.rb64
-rw-r--r--app/models/cycle_analytics.rb68
-rw-r--r--app/models/environment.rb15
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/global_milestone.rb33
-rw-r--r--app/models/group.rb15
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/key.rb7
-rw-r--r--app/models/member.rb15
-rw-r--r--app/models/merge_request.rb23
-rw-r--r--app/models/merge_request/metrics.rb1
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/milestone.rb25
-rw-r--r--app/models/namespace.rb10
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb102
-rw-r--r--app/models/project_authorization.rb8
-rw-r--r--app/models/project_feature.rb10
-rw-r--r--app/models/project_group_link.rb7
-rw-r--r--app/models/project_services/chat_service.rb21
-rw-r--r--app/models/project_services/jira_service.rb201
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb49
-rw-r--r--app/models/project_services/slack_service/note_message.rb12
-rw-r--r--app/models/project_services/slack_service/pipeline_message.rb7
-rw-r--r--app/models/project_team.rb133
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/repository.rb450
-rw-r--r--app/models/service.rb8
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/subscription.rb7
-rw-r--r--app/models/tree.rb26
-rw-r--r--app/models/user.rb182
40 files changed, 976 insertions, 682 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bb60cc8736c..bf463a3b6bb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
serialize :repository_storages
+ serialize :sidekiq_throttling_queues, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -85,6 +86,15 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
+ validates :sidekiq_throttling_factor,
+ numericality: { greater_than: 0, less_than: 1 },
+ presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
+ validates :sidekiq_throttling_queues,
+ presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -180,6 +190,7 @@ class ApplicationSetting < ActiveRecord::Base
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
+ sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
@@ -192,6 +203,10 @@ class ApplicationSetting < ActiveRecord::Base
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
+ def sidekiq_throttling_column_exists?
+ ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
+ end
+
def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
end
@@ -245,6 +260,12 @@ class ApplicationSetting < ActiveRecord::Base
ensure_health_check_access_token!
end
+ def sidekiq_throttling_enabled?
+ return false unless sidekiq_throttling_column_exists?
+
+ sidekiq_throttling_enabled
+ end
+
private
def check_repository_storages
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
new file mode 100644
index 00000000000..f321db75eeb
--- /dev/null
+++ b/app/models/chat_name.rb
@@ -0,0 +1,12 @@
+class ChatName < ActiveRecord::Base
+ belongs_to :service
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :service, presence: true
+ validates :team_id, presence: true
+ validates :chat_id, presence: true
+
+ validates :user_id, uniqueness: { scope: [:service_id] }
+ validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf5f92f8462..e7d33bd26db 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -7,6 +7,8 @@ module Ci
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
+ has_many :deployments, as: :deployable
+
serialize :options
serialize :yaml_variables
@@ -68,7 +70,11 @@ module Ci
environment: build.environment,
status_event: 'enqueue'
)
- MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(build.project, nil)
+ .close(new_build)
+
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
@@ -125,6 +131,34 @@ module Ci
!self.pipeline.statuses.latest.include?(self)
end
+ def expanded_environment_name
+ ExpandVariables.expand(environment, variables) if environment
+ end
+
+ def has_environment?
+ self.environment.present?
+ end
+
+ def starts_environment?
+ has_environment? && self.environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment? && self.environment_action == 'stop'
+ end
+
+ def environment_action
+ self.options.fetch(:environment, {}).fetch(:action, 'start')
+ end
+
+ def outdated_deployment?
+ success? && !last_deployment.try(:last?)
+ end
+
+ def last_deployment
+ deployments.last
+ end
+
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -271,6 +305,7 @@ module Ci
def append_trace(trace_part, offset)
recreate_trace_dir
+ touch if needs_touch?
trace_part = hide_secrets(trace_part)
@@ -280,6 +315,10 @@ module Ci
end
end
+ def needs_touch?
+ Time.now - updated_at > 15.minutes.to_i
+ end
+
def trace_file_path
if has_old_trace_file?
old_path_to_trace
@@ -448,6 +487,10 @@ module Ci
]
end
+ def credentials
+ Gitlab::Ci::Build::Credentials::Factory.new(self).create!
+ end
+
private
def update_artifacts_size
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index d159fc6c5c7..c345bf293c9 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -135,15 +135,19 @@ class CommitStatus < ActiveRecord::Base
allow_failure? && (failed? || canceled?)
end
+ def duration
+ calculate_duration
+ end
+
def playable?
false
end
- def duration
- calculate_duration
+ def stuck?
+ false
end
- def stuck?
+ def has_trace?
false
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 664bb594aa9..69d8afc45da 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -215,7 +215,7 @@ module Issuable
end
end
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
participants(user).include?(user)
end
@@ -251,6 +251,17 @@ module Issuable
self.class.to_ability_name
end
+ # Convert this Issuable class name to a format usable by notifications.
+ #
+ # Examples:
+ #
+ # issuable.class # => MergeRequest
+ # issuable.human_class_name # => "merge request"
+
+ def human_class_name
+ @human_class_name ||= self.class.name.titleize.downcase
+ end
+
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index eb2ff0428f6..8ab0401d288 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -1,6 +1,6 @@
# == Mentionable concern
#
-# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by
+# Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
# GFM references.
#
# Used by Issue, Note, MergeRequest, and Commit.
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 7bcc78247ba..e65fc9eaa09 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -23,7 +23,31 @@ module Milestoneish
(due_date - Date.today).to_i
end
+ def elapsed_days
+ return 0 if !start_date || start_date.future?
+
+ (Date.today - start_date).to_i
+ end
+
def issues_visible_to_user(user = nil)
issues.visible_to_user(user)
end
+
+ def upcoming?
+ start_date && start_date.future?
+ end
+
+ def expires_at
+ if due_date
+ if due_date.past?
+ "expired on #{due_date.to_s(:medium)}"
+ else
+ "expires on #{due_date.to_s(:medium)}"
+ end
+ end
+ end
+
+ def expired?
+ due_date && due_date.past?
+ end
end
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
new file mode 100644
index 00000000000..50a1d7fc3e1
--- /dev/null
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -0,0 +1,9 @@
+module SelectForProjectAuthorization
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def select_for_project_authorization
+ select("members.user_id, projects.id AS project_id, members.access_level")
+ end
+ end
+end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 083257f1005..83daa9b1a64 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -12,39 +12,71 @@ module Subscribable
has_many :subscriptions, dependent: :destroy, as: :subscribable
end
- def subscribed?(user)
- if subscription = subscriptions.find_by_user_id(user.id)
+ def subscribed?(user, project = nil)
+ if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
- subscribed_without_subscriptions?(user)
+ subscribed_without_subscriptions?(user, project)
end
end
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
false
end
- def subscribers
- subscriptions.where(subscribed: true).map(&:user)
+ def subscribers(project)
+ subscriptions_available(project).
+ where(subscribed: true).
+ map(&:user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
+ def toggle_subscription(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project).
+ update(subscribed: !subscribed?(user, project))
+ end
+
+ def subscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: true)
+ end
+
+ def unsubscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: false)
end
- def subscribe(user)
+ private
+
+ def unsubscribe_from_other_levels(user, project)
+ other_subscriptions = subscriptions.where(user: user)
+
+ other_subscriptions =
+ if project.blank?
+ other_subscriptions.where.not(project: nil)
+ else
+ other_subscriptions.where(project: nil)
+ end
+
+ other_subscriptions.update_all(subscribed: false)
+ end
+
+ def find_or_initialize_subscription(user, project)
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: true)
+ find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
end
- def unsubscribe(user)
+ def subscriptions_available(project)
+ t = Subscription.arel_table
+
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: false)
+ where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id))))
end
end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index 8ed4a56b19b..cb8e088d21d 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,103 +1,61 @@
class CycleAnalytics
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
+ STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, from:)
@project = project
@from = from
+ @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
end
def summary
@summary ||= Summary.new(@project, from: @from)
end
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
+ end
+
def issue
- calculate_metric(:issue,
+ @fetcher.calculate_metric(:issue,
Issue.arel_table[:created_at],
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]])
end
def plan
- calculate_metric(:plan,
+ @fetcher.calculate_metric(:plan,
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]],
Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
end
def code
- calculate_metric(:code,
+ @fetcher.calculate_metric(:code,
Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
MergeRequest.arel_table[:created_at])
end
def test
- calculate_metric(:test,
+ @fetcher.calculate_metric(:test,
MergeRequest::Metrics.arel_table[:latest_build_started_at],
MergeRequest::Metrics.arel_table[:latest_build_finished_at])
end
def review
- calculate_metric(:review,
+ @fetcher.calculate_metric(:review,
MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at])
end
def staging
- calculate_metric(:staging,
+ @fetcher.calculate_metric(:staging,
MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
def production
- calculate_metric(:production,
+ @fetcher.calculate_metric(:production,
Issue.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
-
- private
-
- def calculate_metric(name, start_time_attrs, end_time_attrs)
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> pair (where the merge request
- # closes the given issue) with issue and merge request metrics included. The metrics
- # are loaded with an inner join, so issues / merge requests without metrics are
- # automatically excluded.
- def base_query_for(name)
- arel_table = MergeRequestsClosingIssues.arel_table
-
- # Load issues
- query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
- join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
- where(Issue.arel_table[:project_id].eq(@project.id)).
- where(Issue.arel_table[:deleted_at].eq(nil)).
- where(Issue.arel_table[:created_at].gteq(@from))
-
- # Load merge_requests
- query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
- on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
- join(MergeRequest::Metrics.arel_table).
- on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 73f415c0ef0..a7f4156fc2e 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base
allow_nil: true,
addressable_url: true
- delegate :stop_action, to: :last_deployment, allow_nil: true
+ delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
@@ -37,6 +37,10 @@ class Environment < ActiveRecord::Base
state :stopped
end
+ def recently_updated_on_branch?(ref)
+ ref.to_s == last_deployment.try(:ref)
+ end
+
def last_deployment
deployments.last
end
@@ -92,6 +96,15 @@ class Environment < ActiveRecord::Base
def stop!(current_user)
return unless stoppable?
+ stop
stop_action.play(current_user)
end
+
+ def actions_for(environment)
+ return [] unless manual_actions
+
+ manual_actions.select do |action|
+ action.expanded_environment_name == environment
+ end
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index c76d88b1c7b..21eaca917b8 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -62,7 +62,7 @@ class Event < ActiveRecord::Base
end
def visible_to_user?(user = nil)
- if push?
+ if push? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
@@ -283,7 +283,7 @@ class Event < ActiveRecord::Base
end
def commit_note?
- target.for_commit?
+ note? && target && target.for_commit?
end
def issue_note?
@@ -295,7 +295,7 @@ class Event < ActiveRecord::Base
end
def project_snippet_note?
- target.for_snippet?
+ note? && target && target.for_snippet?
end
def note_target
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index cde4a568577..b01607dcda9 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -28,26 +28,16 @@ class GlobalMilestone
@title.to_slug.normalize.to_s
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
- end
-
def projects
@projects ||= Project.for_milestones(milestones.select(:id))
end
def state
- state = milestones.map { |milestone| milestone.state }
-
- if state.count('closed') == state.size
- 'closed'
- else
- 'active'
+ milestones.each do |milestone|
+ return 'active' if milestone.state != 'closed'
end
+
+ 'closed'
end
def active?
@@ -81,18 +71,15 @@ class GlobalMilestone
@due_date =
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
@milestones.first.due_date
- else
- nil
end
end
- def expires_at
- if due_date
- if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
- else
- "expires on #{due_date.to_s(:medium)}"
+ def start_date
+ return @start_date if defined?(@start_date)
+
+ @start_date =
+ if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
+ @milestones.first.start_date
end
- end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index d9e90cd256a..4248e1162d8 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -5,6 +5,7 @@ class Group < Namespace
include Gitlab::VisibilityLevel
include AccessRequestable
include Referable
+ include SelectForProjectAuthorization
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :group_members
@@ -61,6 +62,16 @@ class Group < Namespace
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
+
+ def select_for_project_authorization
+ if current_scope.joins_values.include?(:shared_projects)
+ joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
+ .where('project_namespace.share_with_group_lock = ?', false)
+ .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ else
+ super
+ end
+ end
end
def to_reference(_from_project = nil)
@@ -176,4 +187,8 @@ class Group < Namespace
def system_hook_service
SystemHooksService.new
end
+
+ def refresh_members_authorized_projects
+ UserProjectAccessChangedService.new(users.pluck(:id)).execute
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index adbca510ef7..dd0cb75f9a8 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -93,7 +93,7 @@ class Issue < ActiveRecord::Base
# Check if we are scoped to a specific project's issues
if owner_project
- if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER)
+ if owner_project.team.member?(user, Gitlab::Access::REPORTER)
# If the project is authorized for the user, they can see all issues in the project
return all
else
@@ -266,7 +266,7 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+ json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
if options.has_key?(:labels)
json[:labels] = labels.as_json(
diff --git a/app/models/key.rb b/app/models/key.rb
index 568a60b8af3..ff8dda2dc89 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -6,7 +6,7 @@ class Key < ActiveRecord::Base
belongs_to :user
- before_validation :strip_white_space, :generate_fingerprint
+ before_validation :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
@@ -21,8 +21,9 @@ class Key < ActiveRecord::Base
after_destroy :remove_from_shell
after_destroy :post_destroy_hook
- def strip_white_space
- self.key = key.strip unless key.blank?
+ def key=(value)
+ value.strip! unless value.blank?
+ write_attribute(:key, value)
end
def publishable_key
diff --git a/app/models/member.rb b/app/models/member.rb
index b89ba8ecbb8..df93aaee847 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -113,6 +113,8 @@ class Member < ActiveRecord::Base
member.save
end
+ UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
+
member
end
@@ -239,17 +241,28 @@ class Member < ActiveRecord::Base
end
def post_create_hook
+ UserProjectAccessChangedService.new(user.id).execute
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
- # override in subclass
+ UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
end
def post_destroy_hook
+ refresh_member_authorized_projects
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def refresh_member_authorized_projects
+ # If user/source is being destroyed, project access are gonna be destroyed eventually
+ # because of DB foreign keys, so we shouldn't bother with refreshing after each
+ # member is destroyed through association
+ return if destroyed_by_association.present?
+
+ UserProjectAccessChangedService.new(user_id).execute
+ end
+
def after_accept_invite
post_create_hook
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d76feb9680e..fdf54cc8a7e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -494,10 +494,14 @@ class MergeRequest < ActiveRecord::Base
discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
end
+ def discussions_to_be_resolved?
+ discussions_resolvable? && !discussions_resolved?
+ end
+
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
- discussions_resolved?
+ !discussions_to_be_resolved?
end
def hook_attrs
@@ -686,18 +690,21 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_build_succeeds?
- !pipeline || pipeline.success?
+ !pipeline || pipeline.success? || pipeline.skipped?
end
def environments
return [] unless diff_head_commit
- @environments ||=
- begin
- envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
- envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
- envs.uniq
- end
+ @environments ||= begin
+ target_envs = target_project.environments_for(
+ target_branch, commit: diff_head_commit, with_tags: true)
+
+ source_envs = source_project.environments_for(
+ source_branch, commit: diff_head_commit) if source_project
+
+ (target_envs.to_a + source_envs.to_a).uniq
+ end
end
def state_human_name
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 99c49a020c9..cdc408738be 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,5 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index dd65a9a8b86..58a24eb84cb 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
+ serialize :st_commits
+ serialize :st_diffs
+
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit
end
- serialize :st_commits
- serialize :st_diffs
+ scope :viewable, -> { without_state(:empty) }
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 23aecbfa3a6..c774e69080c 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
+ validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title
@@ -131,24 +132,6 @@ class Milestone < ActiveRecord::Base
self.title
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
- end
-
- def expires_at
- if due_date
- if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
- else
- "expires on #{due_date.to_s(:medium)}"
- end
- end
- end
-
def can_be_closed?
active? && issues.opened.count.zero?
end
@@ -212,4 +195,10 @@ class Milestone < ActiveRecord::Base
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
+
+ def start_date_should_be_less_than_due_date
+ if due_date <= start_date
+ errors.add(:start_date, "Can't be greater than due date")
+ end
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index b67049f0f55..891dffac648 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -27,6 +27,7 @@ class Namespace < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
+ after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy
before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
@@ -103,6 +104,8 @@ class Namespace < ActiveRecord::Base
gitlab_shell.add_namespace(repository_storage_path, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('namespace directory cannot be moved')
@@ -175,4 +178,11 @@ class Namespace < ActiveRecord::Base
end
end
end
+
+ def refresh_access_of_projects_invited_groups
+ Group.
+ joins(project_group_links: :project).
+ where(projects: { namespace_id: id }).
+ find_each(&:refresh_members_authorized_projects)
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2d644b03e4d..ed4224e3046 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -7,6 +7,7 @@ class Note < ActiveRecord::Base
include Importable
include FasterCacheKeys
include CacheMarkdownField
+ include AfterCommitQueue
cache_markdown_field :note, pipeline: :note
@@ -18,6 +19,9 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count
+ # Attribute used to store the attributes that have ben changed by slash commands.
+ attr_accessor :commands_changes
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
diff --git a/app/models/project.rb b/app/models/project.rb
index bbe590b5a8a..9256e9ddd95 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -13,6 +13,7 @@ class Project < ActiveRecord::Base
include CaseSensitivity
include TokenAuthenticatable
include ProjectFeaturesCompatibility
+ include SelectForProjectAuthorization
extend Gitlab::ConfigHelper
@@ -23,7 +24,9 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
+ :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
+ allow_nil: true
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
@@ -35,6 +38,7 @@ class Project < ActiveRecord::Base
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 :only_allow_merge_if_all_discussions_are_resolved, false
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
@@ -74,9 +78,9 @@ class Project < ActiveRecord::Base
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
+ has_many :chat_services
# Project services
- has_many :services
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
@@ -89,6 +93,7 @@ class Project < ActiveRecord::Base
has_one :assembla_service, dependent: :destroy
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
+ has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
@@ -121,6 +126,8 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
+ has_many :project_authorizations, dependent: :destroy
+ has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
has_many :users, through: :project_members
@@ -158,6 +165,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
+ delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
# Validations
validates :creator, presence: true, on: :create
@@ -169,6 +177,7 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
+ project_path: true,
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
@@ -748,27 +757,32 @@ class Project < ActiveRecord::Base
update_column(:has_external_wiki, services.external_wikis.any?)
end
- def build_missing_services
+ def find_or_initialize_services
services_templates = Service.where(template: true)
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service = find_service(services, service_name)
- # If service is available but missing in db
- if service.nil?
+ if service
+ service
+ else
# We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
- # If no template, we should create an instance. Ex `create_gitlab_ci_service`
- public_send("create_#{service_name}_service")
+ # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ public_send("build_#{service_name}_service")
else
- Service.create_from_template(self.id, template)
+ Service.build_from_template(id, template)
end
end
end
end
+ def find_or_initialize_service(name)
+ find_or_initialize_services.find { |service| service.to_param == name }
+ end
+
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
@@ -878,7 +892,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || !repository.has_visible_content?
+ repository.empty_repo?
end
def repo
@@ -1076,7 +1090,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
- repository.expire_avatar_cache(branch)
+ repository.expire_avatar_cache
reload_default_branch
end
@@ -1282,20 +1296,6 @@ class Project < ActiveRecord::Base
end
end
- # Checks if `user` is authorized for this project, with at least the
- # `min_access_level` (if given).
- #
- # If you change the logic of this method, please also update `User#authorized_projects`
- def authorized_for_user?(user, min_access_level = nil)
- return false unless user
-
- return true if personal? && namespace_id == user.namespace_id
-
- authorized_for_user_by_group?(user, min_access_level) ||
- authorized_for_user_by_members?(user, min_access_level) ||
- 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)
@@ -1318,22 +1318,30 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit, with_tags: false)
- environment_ids = deployments.group(:environment_id).
- select(:environment_id)
+ def environments_for(ref, commit: nil, with_tags: false)
+ deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
- environment_ids =
- if with_tags
- environment_ids.where('ref=? OR tag IS TRUE', ref)
- else
- environment_ids.where(ref: ref)
- end
+ environment_ids = deployments
+ .where(deployments_query, ref.to_s)
+ .group(:environment_id)
+ .select(:environment_id)
- environments.available.where(id: environment_ids).select do |environment|
+ environments_found = environments.available
+ .where(id: environment_ids).to_a
+
+ return environments_found unless commit
+
+ environments_found.select do |environment|
environment.includes_commit?(commit)
end
end
+ def environments_recently_updated_on_branch(branch)
+ environments_for(branch).select do |environment|
+ environment.recently_updated_on_branch?(branch)
+ end
+ end
+
private
def pushes_since_gc_redis_key
@@ -1345,30 +1353,6 @@ class Project < ActiveRecord::Base
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)
-
- member && (!min_access_level || member.access_level >= min_access_level)
- end
-
- def authorized_for_user_by_members?(user, min_access_level)
- member = members.find_by(user_id: user)
-
- member && (!min_access_level || member.access_level >= min_access_level)
- end
-
- def authorized_for_user_by_shared_projects?(user, min_access_level)
- shared_projects = user.group_members.joins(group: :shared_projects).
- where(project_group_links: { project_id: self })
-
- if min_access_level
- members_scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
- shared_projects = shared_projects.where(members: members_scope)
- end
-
- shared_projects.any?
- end
-
# Similar to the normal callbacks that hook into the life cycle of an
# Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
new file mode 100644
index 00000000000..a00d43773d9
--- /dev/null
+++ b/app/models/project_authorization.rb
@@ -0,0 +1,8 @@
+class ProjectAuthorization < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 34fd5a57b5e..03194fc2141 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -49,23 +49,21 @@ class ProjectFeature < ActiveRecord::Base
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
+ def issues_enabled?
+ issues_access_level > DISABLED
+ end
+
private
# Validates builds and merge requests access level
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index db46def11eb..6149c35cc61 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -16,6 +16,9 @@ class ProjectGroupLink < ActiveRecord::Base
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
+ after_create :refresh_group_members_authorized_projects
+ after_destroy :refresh_group_members_authorized_projects
+
def self.access_options
Gitlab::Access.options
end
@@ -35,4 +38,8 @@ class ProjectGroupLink < ActiveRecord::Base
errors.add(:base, "Project cannot be shared with the project it is in.")
end
end
+
+ def refresh_group_members_authorized_projects
+ group.refresh_members_authorized_projects
+ end
end
diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb
new file mode 100644
index 00000000000..d36beff5fa6
--- /dev/null
+++ b/app/models/project_services/chat_service.rb
@@ -0,0 +1,21 @@
+# Base class for Chat services
+# This class is not meant to be used directly, but only to inherrit from.
+class ChatService < Service
+ default_value_for :category, 'chat'
+
+ has_many :chat_names, foreign_key: :service_id
+
+ def valid_token?(token)
+ self.respond_to?(:token) &&
+ self.token.present? &&
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
+ end
+
+ def supported_events
+ []
+ end
+
+ def trigger(params)
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2dbe0075465..70bbbbcda85 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,24 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-# build_events :boolean default(FALSE), not null
-#
-
class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
@@ -30,6 +9,10 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ def supported_events
+ %w(commit merge_request)
+ end
+
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
@@ -70,13 +53,13 @@ class JiraService < IssueTrackerService
end
def jira_project
- @jira_project ||= client.Project.find(project_key)
+ @jira_project ||= jira_request { client.Project.find(project_key) }
end
def help
- 'See the ' \
- '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
- 'for details.'
+ 'You need to configure JIRA before enabling this service. For more details
+ read the
+ [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).'
end
def title
@@ -128,21 +111,26 @@ class JiraService < IssueTrackerService
# we just want to test settings
test_settings
else
- close_issue(push, issue)
+ jira_issue = jira_request { client.Issue.find(issue.iid) }
+
+ return false unless jira_issue.present?
+
+ close_issue(push, jira_issue)
end
end
def create_cross_reference_note(mentioned, noteable, author)
- issue_key = mentioned.id
- project = self.project
- noteable_name = noteable.class.name.underscore.downcase
- noteable_id = if noteable.is_a?(Commit)
- noteable.id
- else
- noteable.iid
- end
+ unless can_cross_reference?(noteable)
+ return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled."
+ end
+
+ jira_issue = jira_request { client.Issue.find(mentioned.id) }
+
+ return unless jira_issue.present?
- entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
+ noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
+ noteable_type = noteable_name(noteable)
+ entity_url = build_entity_url(noteable_type, noteable_id)
data = {
user: {
@@ -150,17 +138,17 @@ class JiraService < IssueTrackerService
url: resource_url(user_path(author)),
},
project: {
- name: project.path_with_namespace,
- url: resource_url(namespace_project_path(project.namespace, project))
+ name: self.project.path_with_namespace,
+ url: resource_url(namespace_project_path(project.namespace, self.project))
},
entity: {
- name: noteable_name.humanize.downcase,
+ name: noteable_type.humanize.downcase,
url: entity_url,
title: noteable.title
}
}
- add_comment(data, issue_key)
+ add_comment(data, jira_issue)
end
# reason why service cannot be tested
@@ -181,16 +169,22 @@ class JiraService < IssueTrackerService
def test_settings
return unless url.present?
# Test settings by getting the project
- jira_project
-
- rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
- false
+ jira_request { jira_project.present? }
end
private
+ def can_cross_reference?(noteable)
+ case noteable
+ when Commit then commit_events
+ when MergeRequest then merge_requests_events
+ else true
+ end
+ end
+
def close_issue(entity, issue)
+ return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
+
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
@@ -200,72 +194,117 @@ class JiraService < IssueTrackerService
commit_url = build_entity_url(:commit, commit_id)
# Depending on the JIRA project's workflow, a comment during transition
- # may or may not be allowed. Split the operation in to two calls so the
- # comment always works.
- transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url)
+ # may or may not be allowed. Refresh the issue after transition and check
+ # if it is closed, so we don't have one comment for every commit.
+ issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution
end
def transition_issue(issue)
- issue = client.Issue.find(issue.iid)
issue.transitions.build.save(transition: { id: jira_issue_transition_id })
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- comment = "Issue solved with [#{commit_id}|#{commit_url}]."
- send_message(issue.iid, comment)
+ link_title = "GitLab: Solved by commit #{commit_id}."
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
+ send_message(issue, comment, link_props)
end
- def add_comment(data, issue_key)
- user_name = data[:user][:name]
- user_url = data[:user][:url]
- entity_name = data[:entity][:name]
- entity_url = data[:entity][:url]
+ def add_comment(data, issue)
+ user_name = data[:user][:name]
+ user_url = data[:user][:url]
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
+ link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue_key, message)
- send_message(issue_key, message)
+ unless comment_exists?(issue, message)
+ send_message(issue, message, link_props)
end
end
- def comment_exists?(issue_key, message)
- comments = client.Issue.find(issue_key).comments
- comments.map { |comment| comment.body.include?(message) }.any?
+ def comment_exists?(issue, message)
+ comments = jira_request { issue.comments }
+
+ comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
- def send_message(issue_key, message)
+ def send_message(issue, message, remote_link_props)
return unless url.present?
- issue = client.Issue.find(issue_key)
+ jira_request do
+ if issue.comments.build.save!(body: message)
+ remote_link = issue.remotelink.build
+ remote_link.save!(remote_link_props)
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ end
- if issue.comments.build.save!(body: message)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ Rails.logger.info(result_message)
+ result_message
end
+ end
- Rails.logger.info(result_message)
- result_message
- rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ # Build remote link on JIRA properties
+ # Icons here must be available on WEB so JIRA can read the URL
+ # We are using a open word graphics icon which have LGPL license
+ def build_remote_link_props(url:, title:, resolved: false)
+ status = {
+ resolved: resolved
+ }
+
+ if resolved
+ status[:icon] = {
+ title: 'Closed',
+ url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
+ }
+ end
+
+ {
+ GlobalID: 'GitLab',
+ object: {
+ url: url,
+ title: title,
+ status: status,
+ icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ }
+ }
end
def resource_url(resource)
"#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end
- def build_entity_url(entity_name, entity_id)
- resource_url(
- polymorphic_url(
- [
- self.project.namespace.becomes(Namespace),
- self.project,
- entity_name
- ],
- id: entity_id,
- routing_type: :path
- )
+ def build_entity_url(noteable_type, entity_id)
+ polymorphic_url(
+ [
+ self.project.namespace.becomes(Namespace),
+ self.project,
+ noteable_type.to_sym
+ ],
+ id: entity_id,
+ host: Settings.gitlab.base_url
)
end
+
+ def noteable_name(noteable)
+ name = noteable.model_name.singular
+
+ # ProjectSnippet inherits from Snippet class so it causes
+ # routing error building the URL.
+ name == "project_snippet" ? "snippet" : name
+ end
+
+ # Handle errors when doing JIRA API calls
+ def jira_request
+ yield
+
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ nil
+ end
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
new file mode 100644
index 00000000000..33431f41dc2
--- /dev/null
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -0,0 +1,49 @@
+class MattermostSlashCommandsService < ChatService
+ include TriggersHelper
+
+ prop_accessor :token
+
+ def can_test?
+ false
+ end
+
+ def title
+ 'Mattermost Command'
+ end
+
+ def description
+ "Perform common operations on GitLab in Mattermost"
+ end
+
+ def to_param
+ 'mattermost_slash_commands'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '' }
+ ]
+ end
+
+ def trigger(params)
+ return nil unless valid_token?(params[:token])
+
+ user = find_chat_user(params)
+ unless user
+ url = authorize_chat_name_url(params)
+ return Mattermost::Presenter.authorize_chat_name(url)
+ end
+
+ Gitlab::ChatCommands::Command.new(project, user, params).execute
+ end
+
+ private
+
+ def find_chat_user(params)
+ ChatNames::FindUserService.new(self, params).execute
+ end
+
+ def authorize_chat_name_url(params)
+ ChatNames::AuthorizeUserService.new(self, params).execute
+ end
+end
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 9e84e90f38c..797c5937f09 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -46,25 +46,25 @@ class SlackService
commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha)
commented_on_message(
- "[commit #{commit_sha}](#{@note_url})",
+ "commit #{commit_sha}",
format_title(commit[:message]))
end
def create_issue_note(issue)
commented_on_message(
- "[issue ##{issue[:iid]}](#{@note_url})",
+ "issue ##{issue[:iid]}",
format_title(issue[:title]))
end
def create_merge_note(merge_request)
commented_on_message(
- "[merge request !#{merge_request[:iid]}](#{@note_url})",
+ "merge request !#{merge_request[:iid]}",
format_title(merge_request[:title]))
end
def create_snippet_note(snippet)
commented_on_message(
- "[snippet ##{snippet[:id]}](#{@note_url})",
+ "snippet ##{snippet[:id]}",
format_title(snippet[:title]))
end
@@ -76,8 +76,8 @@ class SlackService
"[#{@project_name}](#{@project_url})"
end
- def commented_on_message(target_link, title)
- @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
+ def commented_on_message(target, title)
+ @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*"
end
end
end
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb
index f06b3562965..f8d03c0e2fa 100644
--- a/app/models/project_services/slack_service/pipeline_message.rb
+++ b/app/models/project_services/slack_service/pipeline_message.rb
@@ -1,11 +1,10 @@
class SlackService
class PipelineMessage < BaseMessage
- attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
+ attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
def initialize(data)
pipeline_attributes = data[:object_attributes]
- @sha = pipeline_attributes[:sha]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@@ -14,7 +13,7 @@ class SlackService
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
- @user_name = data[:commit] && data[:commit][:author_name]
+ @user_name = data[:user] && data[:user][:name]
end
def pretext
@@ -73,7 +72,7 @@ class SlackService
end
def pipeline_link
- "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
+ "[##{pipeline_id}](#{pipeline_url})"
end
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index a6e911df9bd..8a53e974b6f 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,6 +21,22 @@ class ProjectTeam
end
end
+ def add_guest(user, current_user: nil)
+ self << [user, :guest, current_user]
+ end
+
+ def add_reporter(user, current_user: nil)
+ self << [user, :reporter, current_user]
+ end
+
+ def add_developer(user, current_user: nil)
+ self << [user, :developer, current_user]
+ end
+
+ def add_master(user, current_user: nil)
+ self << [user, :master, current_user]
+ end
+
def find_member(user_id)
member = project.members.find_by(user_id: user_id)
@@ -64,19 +80,19 @@ class ProjectTeam
alias_method :users, :members
def guests
- @guests ||= fetch_members(:guests)
+ @guests ||= fetch_members(Gitlab::Access::GUEST)
end
def reporters
- @reporters ||= fetch_members(:reporters)
+ @reporters ||= fetch_members(Gitlab::Access::REPORTER)
end
def developers
- @developers ||= fetch_members(:developers)
+ @developers ||= fetch_members(Gitlab::Access::DEVELOPER)
end
def masters
- @masters ||= fetch_members(:masters)
+ @masters ||= fetch_members(Gitlab::Access::MASTER)
end
def import(source_project, current_user = nil)
@@ -125,8 +141,12 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user, min_member_access = Gitlab::Access::GUEST)
- max_member_access(user.id) >= min_member_access
+ # Checks if `user` is authorized for this project, with at least the
+ # `min_access_level` (if given).
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ user.authorized_project?(project, min_access_level)
end
def human_max_access(user_id)
@@ -149,112 +169,29 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
+ users_access = project.project_authorizations.
+ where(user: user_ids).
+ group(:user_id).
+ maximum(:access_level)
- 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_shared_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.merge!(users_access)
access
end
def max_member_access(user_id)
- max_member_access_for_user_ids([user_id])[user_id]
+ max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS
end
private
- # 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 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 }
-
- # 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)
- project_members = project.members
- group_members = group ? group.members : []
-
- if level
- project_members = project_members.public_send(level)
- group_members = group_members.public_send(level) if group
- end
-
- user_ids = project_members.pluck(:user_id)
-
- invited_members = fetch_invited_members(level)
- user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+ members = project.authorized_users
+ members = members.where(project_authorizations: { access_level: level }) if level
- user_ids.push(*group_members.pluck(:user_id)) if group
-
- User.where(id: user_ids)
+ members
end
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
-
- def project_shared_with_group?
- project.invited_groups.any? && project.allowed_to_share_with_group?
- end
-
- def fetch_invited_members(level = nil)
- invited_members = []
-
- return invited_members unless project_shared_with_group?
-
- project.project_group_links.includes(group: [:group_members]).each do |link|
- invited_group_members = link.group.members
-
- if level
- numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
- # If we're asked for a level that's higher than the group's access,
- # there's nothing left to do
- next if numeric_level > link.group_access
-
- # Make sure we include everyone _above_ the requested level as well
- invited_group_members =
- if numeric_level == link.group_access
- invited_group_members.where("access_level >= ?", link.group_access)
- else
- invited_group_members.public_send(level)
- end
- end
-
- invited_members << invited_group_members
- end
-
- invited_members.flatten.compact
- end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 46f70da2452..9db96347322 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -127,7 +127,7 @@ class ProjectWiki
end
def search_files(query)
- repository.search_files(query, default_branch)
+ repository.search_files_by_content(query, default_branch)
end
def repository
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 063dc74021d..bf136ccdb6c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,28 +1,56 @@
require 'securerandom'
class Repository
- class CommitError < StandardError; end
-
- # Files to use as a project avatar in case no avatar was uploaded via the web
- # UI.
- AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
-
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
- def self.storages
- Gitlab.config.repositories.storages
- end
+ class CommitError < StandardError; end
- def self.remove_storage_from_path(repo_path)
- storages.find do |_, storage_path|
- if repo_path.start_with?(storage_path)
- return repo_path.sub(storage_path, '')
- end
+ # Methods that cache data from the Git repository.
+ #
+ # Each entry in this Array should have a corresponding method with the exact
+ # same name. The cache key used by those methods must also match method's
+ # name.
+ #
+ # For example, for entry `:readme` there's a method called `readme` which
+ # stores its data in the `readme` cache key.
+ CACHED_METHODS = %i(size commit_count readme version contribution_guide
+ changelog license_blob license_key gitignore koding_yml
+ gitlab_ci_yml branch_names tag_names branch_count
+ tag_count avatar exists? empty? root_ref)
+
+ # Certain method caches should be refreshed when certain types of files are
+ # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
+ # the corresponding methods to call for refreshing caches.
+ METHOD_CACHES_FOR_FILE_TYPES = {
+ readme: :readme,
+ changelog: :changelog,
+ license: %i(license_blob license_key),
+ contributing: :contribution_guide,
+ version: :version,
+ gitignore: :gitignore,
+ koding: :koding_yml,
+ gitlab_ci: :gitlab_ci_yml,
+ avatar: :avatar
+ }
+
+ # Wraps around the given method and caches its output in Redis and an instance
+ # variable.
+ #
+ # This only works for methods that do not take any arguments.
+ def self.cache_method(name, fallback: nil)
+ original = :"_uncached_#{name}"
+
+ alias_method(original, name)
+
+ define_method(name) do
+ cache_method_output(name, fallback: fallback) { __send__(original) }
end
+ end
- repo_path
+ def self.storages
+ Gitlab.config.repositories.storages
end
def initialize(path_with_namespace, project)
@@ -47,24 +75,6 @@ class Repository
)
end
- def exists?
- return @exists unless @exists.nil?
-
- @exists = cache.fetch(:exists?) do
- begin
- raw_repository && raw_repository.rugged ? true : false
- rescue Gitlab::Git::Repository::NoRepository
- false
- end
- end
- end
-
- def empty?
- return @empty unless @empty.nil?
-
- @empty = cache.fetch(:empty?) { raw_repository.empty? }
- end
-
#
# Git repository can contains some hidden refs like:
# /refs/notes/*
@@ -186,11 +196,18 @@ class Repository
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.tags.create(tag_name, target, options)
+ rugged.tags.create(tag_name, target, options)
+ tag = find_tag(tag_name)
+
+ GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do
+ # we already created a tag, because we need tag SHA to pass correct
+ # values to hooks
end
- find_tag(tag_name)
+ tag
+ rescue GitHooksService::PreReceiveError
+ rugged.tags.delete(tag_name)
+ raise
end
def rm_branch(user, branch_name)
@@ -224,10 +241,6 @@ class Repository
branch_names + tag_names
end
- def branch_names
- @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
- end
-
def branch_exists?(branch_name)
branch_names.include?(branch_name)
end
@@ -277,34 +290,6 @@ class Repository
ref_exists?(keep_around_ref_name(sha))
end
- def tag_names
- cache.fetch(:tag_names) { raw_repository.tag_names }
- end
-
- def commit_count
- cache.fetch(:commit_count) do
- begin
- raw_repository.commit_count(self.root_ref)
- rescue
- 0
- end
- end
- end
-
- def branch_count
- @branch_count ||= cache.fetch(:branch_count) { branches.size }
- end
-
- def tag_count
- @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
- end
-
- # Return repo size in megabytes
- # Cached in redis
- def size
- cache.fetch(:size) { raw_repository.size }
- end
-
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
@@ -320,48 +305,55 @@ class Repository
end
end
- # Keys for data that can be affected for any commit push.
- def cache_keys
- %i(size commit_count
- readme version contribution_guide changelog
- license_blob license_key gitignore koding_yml)
+ def expire_tags_cache
+ expire_method_caches(%i(tag_names tag_count))
+ @tags = nil
end
- # Keys for data on branch/tag operations.
- def cache_keys_for_branches_and_tags
- %i(branch_names tag_names branch_count tag_count)
+ def expire_branches_cache
+ expire_method_caches(%i(branch_names branch_count))
+ @local_branches = nil
end
- def build_cache
- (cache_keys + cache_keys_for_branches_and_tags).each do |key|
- unless cache.exist?(key)
- send(key)
- end
- end
+ def expire_statistics_caches
+ expire_method_caches(%i(size commit_count))
end
- def expire_tags_cache
- cache.expire(:tag_names)
- @tags = nil
+ def expire_all_method_caches
+ expire_method_caches(CACHED_METHODS)
end
- def expire_branches_cache
- cache.expire(:branch_names)
- @branch_names = nil
- @local_branches = nil
+ # Expires the caches of a specific set of methods
+ def expire_method_caches(methods)
+ methods.each do |key|
+ cache.expire(key)
+
+ ivar = cache_instance_variable_name(key)
+
+ remove_instance_variable(ivar) if instance_variable_defined?(ivar)
+ end
end
- def expire_cache(branch_name = nil, revision = nil)
- cache_keys.each do |key|
- cache.expire(key)
+ def expire_avatar_cache
+ expire_method_caches(%i(avatar))
+ end
+
+ # Refreshes the method caches of this repository.
+ #
+ # types - An Array of file types (e.g. `:readme`) used to refresh extra
+ # caches.
+ def refresh_method_caches(types)
+ to_refresh = []
+
+ types.each do |type|
+ methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]
+
+ to_refresh.concat(Array(methods)) if methods
end
- expire_branch_cache(branch_name)
- expire_avatar_cache(branch_name, revision)
+ expire_method_caches(to_refresh)
- # This ensures this particular cache is flushed after the first commit to a
- # new repository.
- expire_emptiness_caches if empty?
+ to_refresh.each { |method| send(method) }
end
def expire_branch_cache(branch_name = nil)
@@ -380,15 +372,14 @@ class Repository
end
def expire_root_ref_cache
- cache.expire(:root_ref)
- @root_ref = nil
+ expire_method_caches(%i(root_ref))
end
# Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches
- cache.expire(:empty?)
- @empty = nil
+ return unless empty?
+ expire_method_caches(%i(empty?))
expire_has_visible_content_cache
end
@@ -397,51 +388,22 @@ class Repository
@has_visible_content = nil
end
- def expire_branch_count_cache
- cache.expire(:branch_count)
- @branch_count = nil
- end
-
- def expire_tag_count_cache
- cache.expire(:tag_count)
- @tag_count = nil
- end
-
def lookup_cache
@lookup_cache ||= {}
end
- def expire_avatar_cache(branch_name = nil, revision = nil)
- # Avatars are pulled from the default branch, thus if somebody pushes to a
- # different branch there's no need to expire anything.
- return if branch_name && branch_name != root_ref
-
- # 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.raw_diffs(deltas_only: true).
- any? { |diff| AVATAR_FILES.include?(diff.new_path) }
- end
-
- cache.expire(:avatar)
-
- @avatar = nil
- end
-
def expire_exists_cache
- cache.expire(:exists?)
- @exists = nil
+ expire_method_caches(%i(exists?))
end
# expire cache that doesn't depend on repository data (when expiring)
def expire_content_cache
expire_tags_cache
- expire_tag_count_cache
expire_branches_cache
- expire_branch_count_cache
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
+ expire_statistics_caches
end
# Runs code after a repository has been created.
@@ -456,9 +418,8 @@ class Repository
# Runs code just before a repository is deleted.
def before_delete
expire_exists_cache
-
- expire_cache if exists?
-
+ expire_all_method_caches
+ expire_branch_cache if exists?
expire_content_cache
repository_event(:remove_repository)
@@ -475,9 +436,9 @@ class Repository
# Runs code before pushing (= creating or removing) a tag.
def before_push_tag
- expire_cache
+ expire_statistics_caches
+ expire_emptiness_caches
expire_tags_cache
- expire_tag_count_cache
repository_event(:push_tag)
end
@@ -485,7 +446,7 @@ class Repository
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
- expire_tag_count_cache
+ expire_statistics_caches
repository_event(:remove_tag)
end
@@ -497,12 +458,14 @@ class Repository
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
- build_cache
+ expire_tags_cache
+ expire_branches_cache
end
# Runs code after a new commit has been pushed.
- def after_push_commit(branch_name, revision)
- expire_cache(branch_name, revision)
+ def after_push_commit(branch_name)
+ expire_statistics_caches
+ expire_branch_cache(branch_name)
repository_event(:push_commit, branch: branch_name)
end
@@ -511,7 +474,6 @@ class Repository
def after_create_branch
expire_branches_cache
expire_has_visible_content_cache
- expire_branch_count_cache
repository_event(:push_branch)
end
@@ -526,7 +488,6 @@ class Repository
# Runs code after an existing branch has been removed.
def after_remove_branch
expire_has_visible_content_cache
- expire_branch_count_cache
expire_branches_cache
end
@@ -553,86 +514,127 @@ class Repository
Gitlab::Git::Blob.raw(self, oid)
end
+ def root_ref
+ if raw_repository
+ raw_repository.root_ref
+ else
+ # When the repo does not exist we raise this error so no data is cached.
+ raise Rugged::ReferenceError
+ end
+ end
+ cache_method :root_ref
+
+ def exists?
+ refs_directory_exists?
+ end
+ cache_method :exists?
+
+ def empty?
+ raw_repository.empty?
+ end
+ cache_method :empty?
+
+ # The size of this repository in megabytes.
+ def size
+ exists? ? raw_repository.size : 0.0
+ end
+ cache_method :size, fallback: 0.0
+
+ def commit_count
+ root_ref ? raw_repository.commit_count(root_ref) : 0
+ end
+ cache_method :commit_count, fallback: 0
+
+ def branch_names
+ branches.map(&:name)
+ end
+ cache_method :branch_names, fallback: []
+
+ def tag_names
+ raw_repository.tag_names
+ end
+ cache_method :tag_names, fallback: []
+
+ def branch_count
+ branches.size
+ end
+ cache_method :branch_count, fallback: 0
+
+ def tag_count
+ raw_repository.rugged.tags.count
+ end
+ cache_method :tag_count, fallback: 0
+
+ def avatar
+ if tree = file_on_head(:avatar)
+ tree.path
+ end
+ end
+ cache_method :avatar
+
def readme
- cache.fetch(:readme) { tree(:head).readme }
+ if head = tree(:head)
+ head.readme
+ end
end
+ cache_method :readme
def version
- cache.fetch(:version) do
- tree(:head).blobs.find do |file|
- file.name.casecmp('version').zero?
- end
- end
+ file_on_head(:version)
end
+ cache_method :version
def contribution_guide
- cache.fetch(:contribution_guide) do
- tree(:head).blobs.find do |file|
- file.contributing?
- end
- end
+ file_on_head(:contributing)
end
+ cache_method :contribution_guide
def changelog
- cache.fetch(:changelog) do
- file_on_head(/\A(changelog|history|changes|news)/i)
- end
+ file_on_head(:changelog)
end
+ cache_method :changelog
def license_blob
- return nil unless head_exists?
-
- cache.fetch(:license_blob) do
- file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
- end
+ file_on_head(:license)
end
+ cache_method :license_blob
def license_key
- return nil unless head_exists?
+ return unless exists?
- cache.fetch(:license_key) do
- Licensee.license(path).try(:key)
- end
+ Licensee.license(path).try(:key)
end
+ cache_method :license_key
def gitignore
- return nil if !exists? || empty?
-
- cache.fetch(:gitignore) do
- file_on_head(/\A\.gitignore\z/)
- end
+ file_on_head(:gitignore)
end
+ cache_method :gitignore
def koding_yml
- return nil unless head_exists?
-
- cache.fetch(:koding_yml) do
- file_on_head(/\A\.koding\.yml\z/)
- end
+ file_on_head(:koding)
end
+ cache_method :koding_yml
def gitlab_ci_yml
- return nil unless head_exists?
-
- @gitlab_ci_yml ||= tree(:head).blobs.find do |file|
- file.name == '.gitlab-ci.yml'
- end
- rescue Rugged::ReferenceError
- # For unknow reason spinach scenario "Scenario: I change project path"
- # lead to "Reference 'HEAD' not found" exception from Repository#empty?
- nil
+ file_on_head(:gitlab_ci)
end
+ cache_method :gitlab_ci_yml
def head_commit
@head_commit ||= commit(self.root_ref)
end
def head_tree
- @head_tree ||= Tree.new(self, head_commit.sha, nil)
+ if head_commit
+ @head_tree ||= Tree.new(self, head_commit.sha, nil)
+ end
end
- def tree(sha = :head, path = nil)
+ def tree(sha = :head, path = nil, recursive: false)
if sha == :head
+ return unless head_commit
+
if path.nil?
return head_tree
else
@@ -640,7 +642,7 @@ class Repository
end
end
- Tree.new(self, sha, path)
+ Tree.new(self, sha, path, recursive: recursive)
end
def blob_at_branch(branch_name, path)
@@ -782,10 +784,6 @@ class Repository
@tags ||= raw_repository.tags
end
- def root_ref
- @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
- end
-
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
options = {
@@ -1063,16 +1061,25 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id
end
- def search_files(query, ref)
- unless exists? && has_visible_content? && query.present?
- return []
- end
+ def empty_repo?
+ !exists? || !has_visible_content?
+ end
+
+ def search_files_by_content(query, ref)
+ return [] if empty_repo? || query.blank?
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
+ def search_files_by_name(query, ref)
+ return [] if empty_repo? || query.blank?
+
+ args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
+ Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
@@ -1134,28 +1141,55 @@ class Repository
end
end
- def avatar
- return nil unless exists?
+ # Caches the supplied block both in a cache and in an instance variable.
+ #
+ # The cache key and instance variable are named the same way as the value of
+ # the `key` argument.
+ #
+ # This method will return `nil` if the corresponding instance variable is also
+ # set to `nil`. This ensures we don't keep yielding the block when it returns
+ # `nil`.
+ #
+ # key - The name of the key to cache the data in.
+ # fallback - A value to fall back to in the event of a Git error.
+ def cache_method_output(key, fallback: nil, &block)
+ ivar = cache_instance_variable_name(key)
- @avatar ||= cache.fetch(:avatar) do
- AVATAR_FILES.find do |file|
- blob_at_branch(root_ref, file)
+ if instance_variable_defined?(ivar)
+ instance_variable_get(ivar)
+ else
+ begin
+ instance_variable_set(ivar, cache.fetch(key, &block))
+ rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
+ # if e.g. HEAD or the entire repository doesn't exist we want to
+ # gracefully handle this and not cache anything.
+ fallback
end
end
end
- private
+ def cache_instance_variable_name(key)
+ :"@#{key.to_s.tr('?!', '')}"
+ end
- def cache
- @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
+ def file_on_head(type)
+ if head = tree(:head)
+ head.blobs.find do |file|
+ Gitlab::FileDetector.type_of(file.name) == type
+ end
+ end
end
- def head_exists?
- exists? && !empty? && !rugged.head_unborn?
+ private
+
+ def refs_directory_exists?
+ return false unless path_with_namespace
+
+ File.exist?(File.join(path_to_repo, 'refs'))
end
- def file_on_head(regex)
- tree(:head).blobs.find { |file| file.name =~ regex }
+ def cache
+ @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end
def tags_sorted_by_committed_date
diff --git a/app/models/service.rb b/app/models/service.rb
index 625fbc48302..0c36acfc1b7 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :confidential_issues_events, true
+ default_value_for :commit_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
@@ -202,7 +203,6 @@ class Service < ActiveRecord::Base
bamboo
buildkite
builds_email
- pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -214,6 +214,8 @@ class Service < ActiveRecord::Base
hipchat
irker
jira
+ mattermost_slash_commands
+ pipelines_email
pivotaltracker
pushover
redmine
@@ -222,11 +224,11 @@ class Service < ActiveRecord::Base
]
end
- def self.create_from_template(project_id, template)
+ def self.build_from_template(project_id, template)
service = template.dup
service.template = false
service.project_id = project_id
- service if service.save
+ service
end
private
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 2373b445009..8ff4e7ae718 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base
include Referable
include Sortable
include Awardable
+ include Mentionable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 3b8aa1eb866..17869c8bac2 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,8 +1,9 @@
class Subscription < ActiveRecord::Base
belongs_to :user
+ belongs_to :project
belongs_to :subscribable, polymorphic: true
- validates :user_id,
- uniqueness: { scope: [:subscribable_id, :subscribable_type] },
- presence: true
+ validates :user, :subscribable, presence: true
+
+ validates :project_id, uniqueness: { scope: [:subscribable_id, :subscribable_type, :user_id] }
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 7c4ed6e393b..fe148b0ec65 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -3,21 +3,24 @@ class Tree
attr_accessor :repository, :sha, :path, :entries
- def initialize(repository, sha, path = '/')
+ def initialize(repository, sha, path = '/', recursive: false)
path = '/' if path.blank?
@repository = repository
@sha = sha
@path = path
+ @recursive = recursive
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path)
+ @entries = get_entries(git_repo, @sha, @path, recursive: @recursive)
end
def readme
return @readme if defined?(@readme)
- available_readmes = blobs.select(&:readme?)
+ available_readmes = blobs.select do |blob|
+ Gitlab::FileDetector.type_of(blob.name) == :readme
+ end
previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name)
@@ -58,4 +61,21 @@ class Tree
def sorted_entries
trees + blobs + submodules
end
+
+ private
+
+ def get_entries(git_repo, sha, path, recursive: false)
+ current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
+ end
+ end
+
+ ordered_entries
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3813df6684e..513a19d81d2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,6 +56,7 @@ class User < ActiveRecord::Base
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
+ has_many :chat_names, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
@@ -72,6 +73,8 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
+ has_many :project_authorizations, dependent: :destroy
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :issues, dependent: :destroy, foreign_key: :author_id
@@ -173,7 +176,7 @@ class User < ActiveRecord::Base
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
+ scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
def self.with_two_factor
@@ -226,19 +229,19 @@ class User < ActiveRecord::Base
def filter(filter_name)
case filter_name
when 'admins'
- self.admins
+ admins
when 'blocked'
- self.blocked
+ blocked
when 'two_factor_disabled'
- self.without_two_factor
+ without_two_factor
when 'two_factor_enabled'
- self.with_two_factor
+ with_two_factor
when 'wop'
- self.without_projects
+ without_projects
when 'external'
- self.external
+ external
else
- self.active
+ active
end
end
@@ -288,8 +291,12 @@ class User < ActiveRecord::Base
end
end
+ def find_by_username(username)
+ iwhere(username: username).take
+ end
+
def find_by_username!(username)
- find_by!('lower(username) = ?', username.downcase)
+ iwhere(username: username).take!
end
def find_by_personal_access_token(token_string)
@@ -336,7 +343,7 @@ class User < ActiveRecord::Base
end
def generate_password
- if self.force_random_password
+ if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
end
@@ -377,56 +384,55 @@ class User < ActiveRecord::Base
end
def two_factor_otp_enabled?
- self.otp_required_for_login?
+ otp_required_for_login?
end
def two_factor_u2f_enabled?
- self.u2f_registrations.exists?
+ u2f_registrations.exists?
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
- return if self.errors.key?(:username) &&
- self.errors[:username].include?('has already been taken')
+ return if errors.key?(:username) &&
+ errors[:username].include?('has already been taken')
- namespace_name = self.username
- existing_namespace = Namespace.by_path(namespace_name)
- if existing_namespace && existing_namespace != self.namespace
- self.errors.add(:username, 'has already been taken')
+ existing_namespace = Namespace.by_path(username)
+ if existing_namespace && existing_namespace != namespace
+ errors.add(:username, 'has already been taken')
end
end
def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
+ unless avatar.image?
+ errors.add :avatar, "only images allowed"
end
end
def unique_email
- if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
- self.errors.add(:email, 'has already been taken')
+ if !emails.exists?(email: email) && Email.exists?(email: email)
+ errors.add(:email, 'has already been taken')
end
end
def owns_notification_email
- return if self.temp_oauth_email?
+ return if temp_oauth_email?
- self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
+ errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
end
def owns_public_email
- return if self.public_email.blank?
+ return if public_email.blank?
- self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
+ errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
def update_emails_with_primary_email
- primary_email_record = self.emails.find_by(email: self.email)
+ primary_email_record = emails.find_by(email: email)
if primary_email_record
primary_email_record.destroy
- self.emails.create(email: self.email_was)
+ emails.create(email: email_was)
- self.update_secondary_emails!
+ update_secondary_emails!
end
end
@@ -438,11 +444,44 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns projects user is authorized to access.
- #
- # If you change the logic of this method, please also update `Project#authorized_for_user`
+ def refresh_authorized_projects
+ loop do
+ begin
+ Gitlab::Database.serialized_transaction do
+ project_authorizations.delete_all
+
+ # project_authorizations_union can return multiple records for the same project/user with
+ # different access_level so we take row with the maximum access_level
+ project_authorizations.connection.execute <<-SQL
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ SELECT user_id, project_id, MAX(access_level) AS access_level
+ FROM (#{project_authorizations_union.to_sql}) sub
+ GROUP BY user_id, project_id
+ SQL
+
+ update_column(:authorized_projects_populated, true) unless authorized_projects_populated
+ end
+
+ break
+ # In the event of a concurrent modification Rails raises StatementInvalid.
+ # In this case we want to keep retrying until the transaction succeeds
+ rescue ActiveRecord::StatementInvalid
+ end
+ end
+ end
+
def authorized_projects(min_access_level = nil)
- Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
+ refresh_authorized_projects unless authorized_projects_populated
+
+ # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
+ projects = super()
+ projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
+
+ projects
+ end
+
+ def authorized_project?(project, min_access_level = nil)
+ authorized_projects(min_access_level).exists?({ id: project.id })
end
# Returns the projects this user has reporter (or greater) access to, limited
@@ -456,8 +495,9 @@ class User < ActiveRecord::Base
end
def viewable_starred_projects
- starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
- [Project::PUBLIC, Project::INTERNAL])
+ starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
+ [Project::PUBLIC, Project::INTERNAL],
+ authorized_projects.select(:project_id))
end
def owned_projects
@@ -580,7 +620,7 @@ class User < ActiveRecord::Base
end
def project_deploy_keys
- DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
+ DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
end
def accessible_deploy_keys
@@ -596,38 +636,38 @@ class User < ActiveRecord::Base
end
def sanitize_attrs
- %w(name username skype linkedin twitter).each do |attr|
- value = self.send(attr)
- self.send("#{attr}=", Sanitize.clean(value)) if value.present?
+ %w[name username skype linkedin twitter].each do |attr|
+ value = public_send(attr)
+ public_send("#{attr}=", Sanitize.clean(value)) if value.present?
end
end
def set_notification_email
- if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
- self.notification_email = self.email
+ if notification_email.blank? || !all_emails.include?(notification_email)
+ self.notification_email = email
end
end
def set_public_email
- if self.public_email.blank? || !self.all_emails.include?(self.public_email)
+ if public_email.blank? || !all_emails.include?(public_email)
self.public_email = ''
end
end
def update_secondary_emails!
- self.set_notification_email
- self.set_public_email
- self.save if self.notification_email_changed? || self.public_email_changed?
+ set_notification_email
+ set_public_email
+ save if notification_email_changed? || public_email_changed?
end
def set_projects_limit
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
- return unless self.has_attribute?(:projects_limit)
+ return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
- return unless self.projects_limit.nil? || connection_default_value_defined
+ return unless projects_limit.nil? || connection_default_value_defined
self.projects_limit = current_application_settings.default_projects_limit
end
@@ -657,7 +697,7 @@ class User < ActiveRecord::Base
def with_defaults
User.defaults.each do |k, v|
- self.send("#{k}=", v)
+ public_send("#{k}=", v)
end
self
@@ -677,7 +717,7 @@ class User < ActiveRecord::Base
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
- Event.where(author_id: self.id).
+ Event.where(author_id: id).
order('id DESC').limit(1000).
update_all(updated_at: Time.now)
end
@@ -710,8 +750,8 @@ class User < ActiveRecord::Base
def all_emails
all_emails = []
- all_emails << self.email unless self.temp_oauth_email?
- all_emails.concat(self.emails.map(&:email))
+ all_emails << email unless temp_oauth_email?
+ all_emails.concat(emails.map(&:email))
all_emails
end
@@ -725,21 +765,21 @@ class User < ActiveRecord::Base
def ensure_namespace_correct
# Ensure user has namespace
- self.create_namespace!(path: self.username, name: self.username) unless self.namespace
+ create_namespace!(path: username, name: username) unless namespace
- if self.username_changed?
- self.namespace.update_attributes(path: self.username, name: self.username)
+ if username_changed?
+ namespace.update_attributes(path: username, name: username)
end
end
def post_create_hook
- log_info("User \"#{self.name}\" (#{self.email}) was created")
- notification_service.new_user(self, @reset_token) if self.created_by_id
+ log_info("User \"#{name}\" (#{email}) was created")
+ notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook
- log_info("User \"#{self.name}\" (#{self.email}) was removed")
+ log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
@@ -783,7 +823,7 @@ class User < ActiveRecord::Base
end
def oauth_authorized_tokens
- Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
+ Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end
# Returns the projects a user contributed to in the last year.
@@ -887,16 +927,14 @@ class User < ActiveRecord::Base
private
- def projects_union(min_access_level = nil)
- relations = [personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)]
-
- if min_access_level
- scope = { access_level: Gitlab::Access.all_values.select { |access| access >= min_access_level } }
- relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
- end
+ # Returns a union query of projects that the user is authorized to access
+ def project_authorizations_union
+ relations = [
+ personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
+ groups_projects.select_for_project_authorization,
+ projects.select_for_project_authorization,
+ groups.joins(:shared_projects).select_for_project_authorization
+ ]
Gitlab::SQL::Union.new(relations)
end
@@ -916,7 +954,7 @@ class User < ActiveRecord::Base
end
def ensure_external_user_rights
- return unless self.external?
+ return unless external?
self.can_create_group = false
self.projects_limit = 0
@@ -928,7 +966,7 @@ class User < ActiveRecord::Base
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
- if domain_matches?(blocked_domains, self.email)
+ if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
@@ -936,7 +974,7 @@ class User < ActiveRecord::Base
allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank?
- if domain_matches?(allowed_domains, self.email)
+ if domain_matches?(allowed_domains, email)
valid = true
else
error = "domain is not authorized for sign-up"
@@ -944,7 +982,7 @@ class User < ActiveRecord::Base
end
end
- self.errors.add(:email, error) unless valid
+ errors.add(:email, error) unless valid
valid
end