diff options
| author | Filipa Lacerda <filipa@gitlab.com> | 2017-05-07 15:00:58 +0100 |
|---|---|---|
| committer | Filipa Lacerda <filipa@gitlab.com> | 2017-05-07 15:00:58 +0100 |
| commit | 842918602dbe622dc20593c0abea5293e304ac62 (patch) | |
| tree | c748164aab8cfa43fe3332640c60e3308b4e9c29 /app/models | |
| parent | 214d7880c3d651b367eb73651a6e0e3046868287 (diff) | |
| parent | 6ad3814e1b31bfacfae7a2aabb4e4607b12ca66f (diff) | |
| download | gitlab-ce-remove-old-isobject.tar.gz | |
Merge branch 'master' into remove-old-isobjectremove-old-isobject
* master: (226 commits)
Real time pipeline show action
Fix `Routable.find_by_full_path` on MySQL
add CHANGELOG.md entry for !11138
add tooltips to user contrib graph key
Use an absolute path for locale path in FastGettext config
Colorize labels in issue search field
Fix Karma failures for jQuery deferreds
Reduce risk of deadlocks
Fix failing spec and eslint
Resolve discussions
Resolve discussions
Dry up routable lookups. Fixes #30317
Add “project moved” flash message on redirect
Resolve discussions
Fix Rubocop failures
Index redirect_routes path for LIKE
Add index for source association and for path
Fix or workaround spec failure
Refactor
Delete conflicting redirects
...
Diffstat (limited to 'app/models')
| -rw-r--r-- | app/models/application_setting.rb | 4 | ||||
| -rw-r--r-- | app/models/blob.rb | 1 | ||||
| -rw-r--r-- | app/models/blob_viewer/balsamiq.rb | 12 | ||||
| -rw-r--r-- | app/models/ci/build.rb | 11 | ||||
| -rw-r--r-- | app/models/ci/group.rb | 40 | ||||
| -rw-r--r-- | app/models/ci/stage.rb | 8 | ||||
| -rw-r--r-- | app/models/concerns/issuable.rb | 29 | ||||
| -rw-r--r-- | app/models/concerns/milestoneish.rb | 2 | ||||
| -rw-r--r-- | app/models/concerns/routable.rb | 24 | ||||
| -rw-r--r-- | app/models/deployment.rb | 4 | ||||
| -rw-r--r-- | app/models/global_milestone.rb | 6 | ||||
| -rw-r--r-- | app/models/issue.rb | 35 | ||||
| -rw-r--r-- | app/models/issue_assignee.rb | 13 | ||||
| -rw-r--r-- | app/models/merge_request.rb | 32 | ||||
| -rw-r--r-- | app/models/milestone.rb | 5 | ||||
| -rw-r--r-- | app/models/note.rb | 6 | ||||
| -rw-r--r-- | app/models/redirect_route.rb | 12 | ||||
| -rw-r--r-- | app/models/route.rb | 55 | ||||
| -rw-r--r-- | app/models/snippet.rb | 5 | ||||
| -rw-r--r-- | app/models/system_note_metadata.rb | 2 | ||||
| -rw-r--r-- | app/models/user.rb | 13 |
21 files changed, 260 insertions, 59 deletions
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cf042717c95..54f01f8637e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -62,6 +62,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :sentry_enabled + validates :clientside_sentry_dsn, + presence: true, + if: :clientside_sentry_enabled + validates :akismet_api_key, presence: true, if: :akismet_enabled diff --git a/app/models/blob.rb b/app/models/blob.rb index a4fae22a0c4..eaf0b713122 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -26,6 +26,7 @@ class Blob < SimpleDelegator BlobViewer::Image, BlobViewer::Sketch, + BlobViewer::Balsamiq, BlobViewer::Video, diff --git a/app/models/blob_viewer/balsamiq.rb b/app/models/blob_viewer/balsamiq.rb new file mode 100644 index 00000000000..f982521db99 --- /dev/null +++ b/app/models/blob_viewer/balsamiq.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Balsamiq < Base + include Rich + include ClientSide + + self.partial_name = 'balsamiq' + self.extensions = %w(bmpr) + self.binary = true + self.switcher_icon = 'file-image-o' + self.switcher_title = 'preview' + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b426c27afbb..971ab7cb0ee 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -111,14 +111,9 @@ module Ci end def play(current_user) - # Try to queue a current build - if self.enqueue - self.update(user: current_user) - self - else - # Otherwise we need to create a duplicate - Ci::Build.retry(self, current_user) - end + Ci::PlayBuildService + .new(project, current_user) + .execute(self) end def cancelable? diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb new file mode 100644 index 00000000000..87898b086c6 --- /dev/null +++ b/app/models/ci/group.rb @@ -0,0 +1,40 @@ +module Ci + ## + # This domain model is a representation of a group of jobs that are related + # to each other, like `rspec 0 1`, `rspec 0 2`. + # + # It is not persisted in the database. + # + class Group + include StaticModel + + attr_reader :stage, :name, :jobs + + delegate :size, to: :jobs + + def initialize(stage, name:, jobs:) + @stage = stage + @name = name + @jobs = jobs + end + + def status + @status ||= commit_statuses.status + end + + def detailed_status(current_user) + if jobs.one? + jobs.first.detailed_status(current_user) + else + Gitlab::Ci::Status::Group::Factory + .new(self, current_user).fabricate! + end + end + + private + + def commit_statuses + @commit_statuses ||= CommitStatus.where(id: jobs.map(&:id)) + end + end +end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index e7d6b17d445..9bda3186c30 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -15,6 +15,14 @@ module Ci @warnings = warnings end + def groups + @groups ||= statuses.ordered.latest + .sort_by(&:sortable_name).group_by(&:group_name) + .map do |group_name, grouped_statuses| + Ci::Group.new(self, name: group_name, jobs: grouped_statuses) + end + end + def to_param name end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 26dbf4d9570..075ec575f9d 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -26,8 +26,8 @@ module Issuable cache_markdown_field :description, issuable_state_filter_enabled: true belongs_to :author, class_name: "User" - belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" + belongs_to :last_edited_by, class_name: 'User' belongs_to :milestone has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do def authors_loaded? @@ -65,11 +65,8 @@ module Issuable validates :title, presence: true, length: { maximum: 255 } scope :authored, ->(user) { where(author_id: user) } - scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :recent, -> { reorder(id: :desc) } scope :order_position_asc, -> { reorder(position: :asc) } - scope :assigned, -> { where("assignee_id IS NOT NULL") } - scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } @@ -92,23 +89,14 @@ module Issuable attr_mentionable :description participant :author - participant :assignee participant :notes_with_associations strip_attributes :title acts_as_paranoid - after_save :update_assignee_cache_counts, if: :assignee_id_changed? after_save :record_metrics, unless: :imported? - def update_assignee_cache_counts - # make sure we flush the cache for both the old *and* new assignees(if they exist) - previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was - previous_assignee&.update_cache_counts - assignee&.update_cache_counts - end - # We want to use optimistic lock for cases when only title or description are involved # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html def locking_enabled? @@ -237,10 +225,6 @@ module Issuable today? && created_at == updated_at end - def is_being_reassigned? - assignee_id_changed? - end - def open? opened? || reopened? end @@ -269,7 +253,11 @@ module Issuable # DEPRECATED repository: project.hook_attrs.slice(:name, :url, :description, :homepage) } - hook_data[:assignee] = assignee.hook_attrs if assignee + if self.is_a?(Issue) + hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any? + else + hook_data[:assignee] = assignee.hook_attrs if assignee + end hook_data end @@ -331,11 +319,6 @@ module Issuable false end - def assignee_or_author?(user) - # We're comparing IDs here so we don't need to load any associations. - author_id == user.id || assignee_id == user.id - end - def record_metrics metrics = self.metrics || create_metrics metrics.record! diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index f449229864d..a3472af5c55 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -40,7 +40,7 @@ module Milestoneish def issues_visible_to_user(user) memoize_per_user(user, :issues_visible_to_user) do IssuesFinder.new(user, issues_finder_params) - .execute.where(milestone_id: milestoneish_ids) + .execute.includes(:assignees).where(milestone_id: milestoneish_ids) end end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index b28e05d0c28..c4463abdfe6 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -5,6 +5,7 @@ module Routable included do has_one :route, as: :source, autosave: true, dependent: :destroy + has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy validates_associated :route validates :route, presence: true @@ -26,16 +27,31 @@ module Routable # Klass.find_by_full_path('gitlab-org/gitlab-ce') # # Returns a single object, or nil. - def find_by_full_path(path) + def find_by_full_path(path, follow_redirects: false) # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so # any literal matches come first, for this we have to use "BINARY". # Without this there's still no guarantee in what order MySQL will return # rows. + # + # Why do we do this? + # + # Even though we have Rails validation on Route for unique paths + # (case-insensitive), there are old projects in our DB (and possibly + # clients' DBs) that have the same path with different cases. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/18603. Also note that + # our unique index is case-sensitive in Postgres. binary = Gitlab::Database.mysql? ? 'BINARY' : '' - order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" - - where_full_path_in([path]).reorder(order_sql).take + found = where_full_path_in([path]).reorder(order_sql).take + return found if found + + if follow_redirects + if Gitlab::Database.postgresql? + joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path) + else + joins(:redirect_routes).find_by(redirect_routes: { path: path }) + end + end end # Builds a relation to find multiple objects by their full paths. diff --git a/app/models/deployment.rb b/app/models/deployment.rb index afad001d50f..37adfb4de73 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -85,8 +85,8 @@ class Deployment < ActiveRecord::Base end def stop_action - return nil unless on_stop.present? - return nil unless manual_actions + return unless on_stop.present? + return unless manual_actions @stop_action ||= manual_actions.find_by(name: on_stop) end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 0afbca2cb32..538615130a7 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -36,7 +36,7 @@ class GlobalMilestone closed = count_by_state(milestones_by_state_and_title, 'closed') all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count - { + { opened: opened, closed: closed, all: all @@ -86,7 +86,7 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels) + @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignees, :labels) end def merge_requests @@ -94,7 +94,7 @@ class GlobalMilestone end def participants - @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq + @participants ||= milestones.map(&:participants).flatten.uniq end def labels diff --git a/app/models/issue.rb b/app/models/issue.rb index 78bde6820da..27e3ed9bc7f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -24,10 +24,17 @@ class Issue < ActiveRecord::Base has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + has_many :issue_assignees + has_many :assignees, class_name: "User", through: :issue_assignees + validates :project, presence: true scope :in_projects, ->(project_ids) { where(project_id: project_ids) } + scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } + scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } + scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)} + scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } @@ -37,13 +44,15 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } - scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } + scope :include_associations, -> { includes(:labels, project: :namespace) } after_save :expire_etag_cache attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true + participant :assignees + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -63,10 +72,14 @@ class Issue < ActiveRecord::Base end def hook_attrs + assignee_ids = self.assignee_ids + attrs = { total_time_spent: total_time_spent, human_total_time_spent: human_total_time_spent, - human_time_estimate: human_time_estimate + human_time_estimate: human_time_estimate, + assignee_ids: assignee_ids, + assignee_id: assignee_ids.first # This key is deprecated } attributes.merge!(attrs) @@ -114,6 +127,22 @@ class Issue < ActiveRecord::Base "id DESC") end + # Returns a Hash of attributes to be used for Twitter card metadata + def card_attributes + { + 'Author' => author.try(:name), + 'Assignee' => assignee_list + } + end + + def assignee_or_author?(user) + author_id == user.id || assignees.exists?(user.id) + end + + def assignee_list + assignees.map(&:name).to_sentence + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" @@ -248,7 +277,7 @@ class Issue < ActiveRecord::Base true elsif confidential? author == user || - assignee == user || + assignees.include?(user) || project.team.member?(user, Gitlab::Access::REPORTER) else project.public? || diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb new file mode 100644 index 00000000000..0663d3aaef8 --- /dev/null +++ b/app/models/issue_assignee.rb @@ -0,0 +1,13 @@ +class IssueAssignee < ActiveRecord::Base + extend Gitlab::CurrentSettings + + belongs_to :issue + belongs_to :assignee, class_name: "User", foreign_key: :user_id + + after_create :update_assignee_cache_counts + after_destroy :update_assignee_cache_counts + + def update_assignee_cache_counts + assignee&.update_cache_counts + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 12c5481cd6d..35231bab12e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -17,6 +17,8 @@ class MergeRequest < ActiveRecord::Base has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + belongs_to :assignee, class_name: "User" + serialize :merge_params, Hash after_create :ensure_merge_request_diff, unless: :importing? @@ -114,8 +116,14 @@ class MergeRequest < ActiveRecord::Base scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } + scope :assigned, -> { where("assignee_id IS NOT NULL") } + scope :unassigned, -> { where("assignee_id IS NULL") } + scope :assigned_to, ->(u) { where(assignee_id: u.id)} + + participant :assignee after_save :keep_around_commit + after_save :update_assignee_cache_counts, if: :assignee_id_changed? def self.reference_prefix '!' @@ -177,6 +185,30 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end + def update_assignee_cache_counts + # make sure we flush the cache for both the old *and* new assignees(if they exist) + previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was + previous_assignee&.update_cache_counts + assignee&.update_cache_counts + end + + # Returns a Hash of attributes to be used for Twitter card metadata + def card_attributes + { + 'Author' => author.try(:name), + 'Assignee' => assignee.try(:name) + } + end + + # This method is needed for compatibility with issues to not mess view and other code + def assignees + Array(assignee) + end + + def assignee_or_author?(user) + author_id == user.id || assignee_id == user.id + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 652b1551928..c06bfe0ccdd 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -21,7 +21,6 @@ class Milestone < ActiveRecord::Base has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests - has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee has_many :events, as: :target, dependent: :destroy scope :active, -> { with_state(:active) } @@ -107,6 +106,10 @@ class Milestone < ActiveRecord::Base end end + def participants + User.joins(assigned_issues: :milestone).where("milestones.id = ?", id) + end + def self.sort(method) case method.to_s when 'due_date_asc' diff --git a/app/models/note.rb b/app/models/note.rb index b06985b4a6f..46d0a4f159f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -18,6 +18,11 @@ class Note < ActiveRecord::Base cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true + # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes. + # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102 + alias_attribute :last_edited_at, :updated_at + alias_attribute :last_edited_by, :updated_by + # Attribute containing rendered and redacted Markdown as generated by # Banzai::ObjectRenderer. attr_accessor :redacted_note_html @@ -38,6 +43,7 @@ class Note < ActiveRecord::Base belongs_to :noteable, polymorphic: true, touch: true belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" + belongs_to :last_edited_by, class_name: 'User' has_many :todos, dependent: :destroy has_many :events, as: :target, dependent: :destroy diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb new file mode 100644 index 00000000000..99812bcde53 --- /dev/null +++ b/app/models/redirect_route.rb @@ -0,0 +1,12 @@ +class RedirectRoute < ActiveRecord::Base + belongs_to :source, polymorphic: true + + validates :source, presence: true + + validates :path, + length: { within: 1..255 }, + presence: true, + uniqueness: { case_sensitive: false } + + scope :matching_path_and_descendants, -> (path) { where('redirect_routes.path = ? OR redirect_routes.path LIKE ?', path, "#{sanitize_sql_like(path)}/%") } +end diff --git a/app/models/route.rb b/app/models/route.rb index 4b3efab5c3c..12a7fa3d01b 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -8,29 +8,58 @@ class Route < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } + after_create :delete_conflicting_redirects + after_update :delete_conflicting_redirects, if: :path_changed? + after_update :create_redirect_for_old_path after_update :rename_descendants scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") } def rename_descendants - if path_changed? || name_changed? - descendants = self.class.inside_path(path_was) + return unless path_changed? || name_changed? - descendants.each do |route| - attributes = {} + descendant_routes = self.class.inside_path(path_was) - if path_changed? && route.path.present? - attributes[:path] = route.path.sub(path_was, path) - end + descendant_routes.each do |route| + attributes = {} - if name_changed? && name_was.present? && route.name.present? - attributes[:name] = route.name.sub(name_was, name) - end + if path_changed? && route.path.present? + attributes[:path] = route.path.sub(path_was, path) + end - # Note that update_columns skips validation and callbacks. - # We need this to avoid recursive call of rename_descendants method - route.update_columns(attributes) unless attributes.empty? + if name_changed? && name_was.present? && route.name.present? + attributes[:name] = route.name.sub(name_was, name) + end + + if attributes.present? + old_path = route.path + + # Callbacks must be run manually + route.update_columns(attributes) + + # We are not calling route.delete_conflicting_redirects here, in hopes + # of avoiding deadlocks. The parent (self, in this method) already + # called it, which deletes conflicts for all descendants. + route.create_redirect(old_path) if attributes[:path] end end end + + def delete_conflicting_redirects + conflicting_redirects.delete_all + end + + def conflicting_redirects + RedirectRoute.matching_path_and_descendants(path) + end + + def create_redirect(path) + RedirectRoute.create(source: source, path: path) + end + + private + + def create_redirect_for_old_path + create_redirect(path_was) if path_changed? + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index d8860718cb5..abfbefdf9a0 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -12,6 +12,11 @@ class Snippet < ActiveRecord::Base cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content + # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets. + # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102 + alias_attribute :last_edited_at, :updated_at + alias_attribute :last_edited_by, :updated_by + # If file_name changes, it invalidates content alias_method :default_content_html_invalidator, :content_html_invalidated? def content_html_invalidated? diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 1e6fc837a75..b44f4fe000c 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -1,6 +1,6 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ - commit merge confidential visible label assignee cross_reference + commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged ].freeze diff --git a/app/models/user.rb b/app/models/user.rb index 43c5fdc038d..accaa91b805 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -100,6 +100,10 @@ class User < ActiveRecord::Base has_many :award_emoji, dependent: :destroy has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id + has_many :issue_assignees + has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue + has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" + # Issues that a user owns are expected to be moved to the "ghost" user before # the user is destroyed. If the user owns any issues during deletion, this # should be treated as an exceptional condition. @@ -333,6 +337,11 @@ class User < ActiveRecord::Base find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) end + def find_by_full_path(path, follow_redirects: false) + namespace = Namespace.find_by_full_path(path, follow_redirects: follow_redirects) + namespace&.owner + end + def reference_prefix '@' end @@ -355,6 +364,10 @@ class User < ActiveRecord::Base end end + def full_path + username + end + def self.internal_attributes [:ghost] end |
