diff options
Diffstat (limited to 'app/models/concerns')
-rw-r--r-- | app/models/concerns/discussion_on_diff.rb | 57 | ||||
-rw-r--r-- | app/models/concerns/has_status.rb | 3 | ||||
-rw-r--r-- | app/models/concerns/ignorable_column.rb | 28 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 11 | ||||
-rw-r--r-- | app/models/concerns/note_on_diff.rb | 9 | ||||
-rw-r--r-- | app/models/concerns/noteable.rb | 68 | ||||
-rw-r--r-- | app/models/concerns/protected_branch_access.rb | 16 | ||||
-rw-r--r-- | app/models/concerns/protected_ref.rb | 42 | ||||
-rw-r--r-- | app/models/concerns/protected_ref_access.rb | 18 | ||||
-rw-r--r-- | app/models/concerns/protected_tag_access.rb | 11 | ||||
-rw-r--r-- | app/models/concerns/resolvable_discussion.rb | 103 | ||||
-rw-r--r-- | app/models/concerns/resolvable_note.rb | 72 |
12 files changed, 405 insertions, 33 deletions
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb new file mode 100644 index 00000000000..87db0c810c3 --- /dev/null +++ b/app/models/concerns/discussion_on_diff.rb @@ -0,0 +1,57 @@ +# Contains functionality shared between `DiffDiscussion` and `LegacyDiffDiscussion`. +module DiscussionOnDiff + extend ActiveSupport::Concern + + included do + NUMBER_OF_TRUNCATED_DIFF_LINES = 16 + + memoized_values << :active + + delegate :line_code, + :original_line_code, + :diff_file, + :diff_line, + :for_line?, + :active?, + + to: :first_note + + delegate :file_path, + :blob, + :highlighted_diff_lines, + :diff_lines, + + to: :diff_file, + allow_nil: true + end + + def diff_discussion? + true + end + + def active? + return @active if @active.present? + + @active = first_note.active? + end + + # Returns an array of at most 16 highlighted lines above a diff note + def truncated_diff_lines(highlight: true) + lines = highlight ? highlighted_diff_lines : diff_lines + prev_lines = [] + + lines.each do |line| + if line.meta? + prev_lines.clear + else + prev_lines << line + + break if for_line?(line) + + prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + prev_lines + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 0a1a65da05a..dff7b6e3523 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -68,7 +68,7 @@ module HasStatus end scope :created, -> { where(status: 'created') } - scope :relevant, -> { where.not(status: 'created') } + scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) } scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } @@ -76,6 +76,7 @@ module HasStatus scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } scope :manual, -> { where(status: 'manual') } + scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb new file mode 100644 index 00000000000..eb9f3423e48 --- /dev/null +++ b/app/models/concerns/ignorable_column.rb @@ -0,0 +1,28 @@ +# Module that can be included into a model to make it easier to ignore database +# columns. +# +# Example: +# +# class User < ActiveRecord::Base +# include IgnorableColumn +# +# ignore_column :updated_at +# end +# +module IgnorableColumn + extend ActiveSupport::Concern + + module ClassMethods + def columns + super.reject { |column| ignored_columns.include?(column.name) } + end + + def ignored_columns + @ignored_columns ||= Set.new + end + + def ignore_column(name) + ignored_columns << name.to_s + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index b4dded7e27e..3d2258d5e3e 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -292,17 +292,6 @@ 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/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index b8dd27a7afe..1a5a7007a2b 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -1,3 +1,4 @@ +# Contains functionality shared between `DiffNote` and `LegacyDiffNote`. module NoteOnDiff extend ActiveSupport::Concern @@ -24,12 +25,4 @@ module NoteOnDiff def diff_attributes raise NotImplementedError end - - def can_be_award_emoji? - false - end - - def to_discussion - Discussion.new([self]) - end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb new file mode 100644 index 00000000000..772ff6a6d2f --- /dev/null +++ b/app/models/concerns/noteable.rb @@ -0,0 +1,68 @@ +module Noteable + # Names of all implementers of `Noteable` that support resolvable notes. + RESOLVABLE_TYPES = %w(MergeRequest).freeze + + def base_class_name + self.class.base_class.name + end + + # Convert this Noteable class name to a format usable by notifications. + # + # Examples: + # + # noteable.class # => MergeRequest + # noteable.human_class_name # => "merge request" + def human_class_name + @human_class_name ||= base_class_name.titleize.downcase + end + + def supports_resolvable_notes? + RESOLVABLE_TYPES.include?(base_class_name) + end + + def supports_discussions? + DiscussionNote::NOTEABLE_TYPES.include?(base_class_name) + end + + def discussion_notes + notes + end + + delegate :find_discussion, to: :discussion_notes + + def discussions + @discussions ||= discussion_notes + .inc_relations_for_view + .discussions(self) + end + + def grouped_diff_discussions + # Doesn't use `discussion_notes`, because this may include commit diff notes + # besides MR diff notes, that we do no want to display on the MR Changes tab. + notes.inc_relations_for_view.grouped_diff_discussions + end + + def resolvable_discussions + @resolvable_discussions ||= discussion_notes.resolvable.discussions(self) + end + + def discussions_resolvable? + resolvable_discussions.any?(&:resolvable?) + end + + def discussions_resolved? + discussions_resolvable? && resolvable_discussions.none?(&:to_be_resolved?) + end + + def discussions_to_be_resolved? + discussions_resolvable? && !discussions_resolved? + end + + def discussions_to_be_resolved + @discussions_to_be_resolved ||= resolvable_discussions.select(&:to_be_resolved?) + end + + def discussions_can_be_resolved_by?(user) + discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) } + end +end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 9dd4d9c6f24..c41b807df8a 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,20 +2,10 @@ module ProtectedBranchAccess extend ActiveSupport::Concern included do - belongs_to :protected_branch - delegate :project, to: :protected_branch - - scope :master, -> { where(access_level: Gitlab::Access::MASTER) } - scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } - end + include ProtectedRefAccess - def humanize - self.class.human_access_levels[self.access_level] - end - - def check_access(user) - return true if user.is_admin? + belongs_to :protected_branch - project.team.max_member_access(user.id) >= access_level + delegate :project, to: :protected_branch end end diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb new file mode 100644 index 00000000000..62eaec2407f --- /dev/null +++ b/app/models/concerns/protected_ref.rb @@ -0,0 +1,42 @@ +module ProtectedRef + extend ActiveSupport::Concern + + included do + belongs_to :project + + validates :name, presence: true + validates :project, presence: true + + delegate :matching, :matches?, :wildcard?, to: :ref_matcher + + def self.protected_ref_accessible_to?(ref, user, action:) + access_levels_for_ref(ref, action: action).any? do |access_level| + access_level.check_access(user) + end + end + + def self.developers_can?(action, ref) + access_levels_for_ref(ref, action: action).any? do |access_level| + access_level.access_level == Gitlab::Access::DEVELOPER + end + end + + def self.access_levels_for_ref(ref, action:) + self.matching(ref).map(&:"#{action}_access_levels").flatten + end + + def self.matching(ref_name, protected_refs: nil) + ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) + end + end + + def commit + project.commit(self.name) + end + + private + + def ref_matcher + @ref_matcher ||= ProtectedRefMatcher.new(self) + end +end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb new file mode 100644 index 00000000000..c4f158e569a --- /dev/null +++ b/app/models/concerns/protected_ref_access.rb @@ -0,0 +1,18 @@ +module ProtectedRefAccess + extend ActiveSupport::Concern + + included do + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end + + def humanize + self.class.human_access_levels[self.access_level] + end + + def check_access(user) + return true if user.admin? + + project.team.max_member_access(user.id) >= access_level + end +end diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb new file mode 100644 index 00000000000..ee65de24dd8 --- /dev/null +++ b/app/models/concerns/protected_tag_access.rb @@ -0,0 +1,11 @@ +module ProtectedTagAccess + extend ActiveSupport::Concern + + included do + include ProtectedRefAccess + + belongs_to :protected_tag + + delegate :project, to: :protected_tag + end +end diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb new file mode 100644 index 00000000000..dd979e7bb17 --- /dev/null +++ b/app/models/concerns/resolvable_discussion.rb @@ -0,0 +1,103 @@ +module ResolvableDiscussion + extend ActiveSupport::Concern + + included do + # A number of properties of this `Discussion`, like `first_note` and `resolvable?`, are memoized. + # When this discussion is resolved or unresolved, the values of these properties potentially change. + # To make sure all memoized values are reset when this happens, `update` resets all instance variables with names in + # `memoized_variables`. If you add a memoized method in `ResolvableDiscussion` or any `Discussion` subclass, + # please make sure the instance variable name is added to `memoized_values`, like below. + cattr_accessor :memoized_values, instance_accessor: false do + [] + end + + memoized_values.push( + :resolvable, + :resolved, + :first_note, + :first_note_to_resolve, + :last_resolved_note, + :last_note + ) + + delegate :potentially_resolvable?, to: :first_note + + delegate :resolved_at, + :resolved_by, + + to: :last_resolved_note, + allow_nil: true + end + + def resolvable? + return @resolvable if @resolvable.present? + + @resolvable = potentially_resolvable? && notes.any?(&:resolvable?) + end + + def resolved? + return @resolved if @resolved.present? + + @resolved = resolvable? && notes.none?(&:to_be_resolved?) + end + + def first_note + @first_note ||= notes.first + end + + def first_note_to_resolve + return unless resolvable? + + @first_note_to_resolve ||= notes.find(&:to_be_resolved?) + end + + def last_resolved_note + return unless resolved? + + @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last + end + + def resolved_notes + notes.select(&:resolved?) + end + + def to_be_resolved? + resolvable? && !resolved? + end + + def can_resolve?(current_user) + return false unless current_user + return false unless resolvable? + + current_user == self.noteable.author || + current_user.can?(:resolve_note, self.project) + end + + def resolve!(current_user) + return unless resolvable? + + update { |notes| notes.resolve!(current_user) } + end + + def unresolve! + return unless resolvable? + + update { |notes| notes.unresolve! } + end + + private + + def update + # Do not select `Note.resolvable`, so that system notes remain in the collection + notes_relation = Note.where(id: notes.map(&:id)) + + yield(notes_relation) + + # Set the notes array to the updated notes + @notes = notes_relation.fresh.to_a + + self.class.memoized_values.each do |var| + instance_variable_set(:"@#{var}", nil) + end + end +end diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb new file mode 100644 index 00000000000..05eb6f86704 --- /dev/null +++ b/app/models/concerns/resolvable_note.rb @@ -0,0 +1,72 @@ +module ResolvableNote + extend ActiveSupport::Concern + + # Names of all subclasses of `Note` that can be resolvable. + RESOLVABLE_TYPES = %w(DiffNote DiscussionNote).freeze + + included do + belongs_to :resolved_by, class_name: "User" + + validates :resolved_by, presence: true, if: :resolved? + + # Keep this scope in sync with `#potentially_resolvable?` + scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable::RESOLVABLE_TYPES) } + # Keep this scope in sync with `#resolvable?` + scope :resolvable, -> { potentially_resolvable.user } + + scope :resolved, -> { resolvable.where.not(resolved_at: nil) } + scope :unresolved, -> { resolvable.where(resolved_at: nil) } + end + + module ClassMethods + # This method must be kept in sync with `#resolve!` + def resolve!(current_user) + unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id) + end + + # This method must be kept in sync with `#unresolve!` + def unresolve! + resolved.update_all(resolved_at: nil, resolved_by_id: nil) + end + end + + # Keep this method in sync with the `potentially_resolvable` scope + def potentially_resolvable? + RESOLVABLE_TYPES.include?(self.class.name) && noteable.supports_resolvable_notes? + end + + # Keep this method in sync with the `resolvable` scope + def resolvable? + potentially_resolvable? && !system? + end + + def resolved? + return false unless resolvable? + + self.resolved_at.present? + end + + def to_be_resolved? + resolvable? && !resolved? + end + + # If you update this method remember to also update `.resolve!` + def resolve!(current_user) + return unless resolvable? + return if resolved? + + self.resolved_at = Time.now + self.resolved_by = current_user + save! + end + + # If you update this method remember to also update `.unresolve!` + def unresolve! + return unless resolvable? + return unless resolved? + + self.resolved_at = nil + self.resolved_by = nil + save! + end +end |