note.rb 9.54 KB
Newer Older
1 2 3 4 5 6 7 8
# == Schema Information
#
# Table name: notes
#
#  id            :integer          not null, primary key
#  note          :text
#  noteable_type :string(255)
#  author_id     :integer
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
9 10
#  created_at    :datetime
#  updated_at    :datetime
11 12 13
#  project_id    :integer
#  attachment    :string(255)
#  line_code     :string(255)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
14 15
#  commit_id     :string(255)
#  noteable_id   :integer
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
16
#  system        :boolean          default(FALSE), not null
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
17
#  st_diff       :text
Stan Hu's avatar
Stan Hu committed
18
#  updated_by_id :integer
Stan Hu's avatar
Stan Hu committed
19
#  is_award      :boolean          default(FALSE), not null
20 21
#

gitlabhq's avatar
gitlabhq committed
22 23 24 25
require 'carrierwave/orm/activerecord'
require 'file_size_validator'

class Note < ActiveRecord::Base
26
  include Gitlab::CurrentSettings
27
  include Participable
28
  include Mentionable
29

30 31
  default_value_for :system, false

32
  attr_mentionable :note, cache: true, pipeline: :note
33
  participant :author
34

gitlabhq's avatar
gitlabhq committed
35
  belongs_to :project
36
  belongs_to :noteable, polymorphic: true, touch: true
Nihad Abbasov's avatar
Nihad Abbasov committed
37
  belongs_to :author, class_name: "User"
38
  belongs_to :updated_by, class_name: "User"
gitlabhq's avatar
gitlabhq committed
39

Nihad Abbasov's avatar
Nihad Abbasov committed
40 41
  delegate :name, to: :project, prefix: true
  delegate :name, :email, to: :author, prefix: true
42

43 44
  before_validation :set_award!

45
  validates :note, :project, presence: true
Valery Sizov's avatar
Valery Sizov committed
46
  validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
47
  validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
48
  validates :line_code, line_code: true, allow_blank: true
49 50
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
gitlabhq's avatar
gitlabhq committed
51

52 53
  validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
  validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
Valery Sizov's avatar
Valery Sizov committed
54
  validates :author, presence: true
55

56
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
57 58

  # Scopes
Valery Sizov's avatar
Valery Sizov committed
59 60
  scope :awards, ->{ where(is_award: true) }
  scope :nonawards, ->{ where(is_award: false) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
61
  scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
62 63
  scope :inline, ->{ where("line_code IS NOT NULL") }
  scope :not_inline, ->{ where(line_code: [nil, '']) }
64
  scope :system, ->{ where(system: true) }
65
  scope :user, ->{ where(system: false) }
66
  scope :common, ->{ where(noteable_type: ["", nil]) }
67
  scope :fresh, ->{ order(created_at: :asc, id: :asc) }
68 69
  scope :inc_author_project, ->{ includes(:project, :author) }
  scope :inc_author, ->{ includes(:author) }
gitlabhq's avatar
gitlabhq committed
70

71
  scope :with_associations, -> do
72
    includes(:author, :noteable, :updated_by,
73
             project: [:project_members, { group: [:group_members] }])
74
  end
gitlabhq's avatar
gitlabhq committed
75

76
  serialize :st_diff
77
  before_create :set_diff, if: ->(n) { n.line_code.present? }
78

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
  class << self
    def discussions_from_notes(notes)
      discussion_ids = []
      discussions = []

      notes.each do |note|
        next if discussion_ids.include?(note.discussion_id)

        # don't group notes for the main target
        if !note.for_diff_line? && note.noteable_type == "MergeRequest"
          discussions << [note]
        else
          discussions << notes.select do |other_note|
            note.discussion_id == other_note.discussion_id
          end
          discussion_ids << note.discussion_id
        end
      end

      discussions
    end
100

101 102 103 104
    def build_discussion_id(type, id, line_code)
      [:discussion, type.try(:underscore), id, line_code].join("-").to_sym
    end

105
    def search(query)
106
      where("LOWER(note) like :query", query: "%#{query.downcase}%")
107
    end
Valery Sizov's avatar
Valery Sizov committed
108 109

    def grouped_awards
110 111
      notes = {}

Valery Sizov's avatar
Valery Sizov committed
112
      awards.select(:note).distinct.map do |note|
113
        notes[note.note] = where(note: note.note)
Valery Sizov's avatar
Valery Sizov committed
114
      end
115 116 117 118 119

      notes["thumbsup"] ||= Note.none
      notes["thumbsdown"] ||= Note.none

      notes
Valery Sizov's avatar
Valery Sizov committed
120
    end
121
  end
122

123
  def cross_reference?
124
    system && SystemNoteService.cross_reference?(note)
125 126
  end

127 128 129 130
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
  end

131
  def find_diff
132
    return nil unless noteable && noteable.diffs.present?
133 134 135

    @diff ||= noteable.diffs.find do |d|
      Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
136
    end
137 138
  end

139 140 141 142
  def hook_attrs
    attributes
  end

143 144 145
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
146
    diff = diff_for_line_code unless for_merge_request?
147 148 149 150 151 152 153 154 155
    diff ||= find_diff

    self.st_diff = diff.to_hash if diff
  end

  def diff
    @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
  end

156 157 158 159
  def diff_for_line_code
    Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
160 161 162
  # Check if such line of code exists in merge request diff
  # If exists - its active discussion
  # If not - its outdated diff
163
  def active?
164
    return true unless self.diff
165
    return false unless noteable
166

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
167 168 169
    noteable.diffs.each do |mr_diff|
      next unless mr_diff.new_path == self.diff.new_path

170
      lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
171 172 173

      lines.each do |line|
        if line.text == diff_line
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
174 175 176 177 178 179 180 181 182 183
          return true
        end
      end
    end

    false
  end

  def outdated?
    !active?
184 185
  end

186
  def diff_file_index
187
    line_code.split('_')[0] if line_code
188 189 190
  end

  def diff_file_name
191
    diff.new_path if diff
192 193
  end

194 195 196 197 198 199 200 201
  def file_path
    if diff.new_path.present?
      diff.new_path
    elsif diff.old_path.present?
      diff.old_path
    end
  end

202
  def diff_old_line
203
    line_code.split('_')[1].to_i if line_code
204 205 206
  end

  def diff_new_line
207
    line_code.split('_')[2].to_i if line_code
208 209
  end

210 211 212 213
  def generate_line_code(line)
    Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
  end

214
  def diff_line
215 216
    return @diff_line if @diff_line

217
    if diff
218
      diff_lines.each do |line|
219 220 221
        if generate_line_code(line) == self.line_code
          @diff_line = line.text
        end
222
      end
223
    end
224 225

    @diff_line
226 227
  end

228 229 230 231 232 233 234 235 236 237 238 239 240 241
  def diff_line_type
    return @diff_line_type if @diff_line_type

    if diff
      diff_lines.each do |line|
        if generate_line_code(line) == self.line_code
          @diff_line_type = line.type
        end
      end
    end

    @diff_line_type
  end

242 243 244 245 246
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

247
    highlighted_diff_lines.each do |line|
248 249 250
      if line.type == "match"
        prev_lines.clear
        prev_match_line = line
251 252
      else
        prev_lines << line
253

254 255 256
        break if generate_line_code(line) == self.line_code

        prev_lines.shift if prev_lines.length >= max_number_of_lines
257 258
      end
    end
259 260

    prev_lines
261 262 263
  end

  def diff_lines
264 265 266 267 268
    @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines)
  end

  def highlighted_diff_lines
    Gitlab::Diff::Highlight.new(diff_lines).highlight
269 270
  end

271
  def discussion_id
272
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
273 274 275 276 277 278 279 280 281 282 283 284 285 286
  end

  def for_commit?
    noteable_type == "Commit"
  end

  def for_commit_diff_line?
    for_commit? && for_diff_line?
  end

  def for_diff_line?
    line_code.present?
  end

Riyad Preukschas's avatar
Riyad Preukschas committed
287 288 289 290
  def for_issue?
    noteable_type == "Issue"
  end

291 292 293 294 295 296
  def for_merge_request?
    noteable_type == "MergeRequest"
  end

  def for_merge_request_diff_line?
    for_merge_request? && for_diff_line?
Cedric Gatay's avatar
Cedric Gatay committed
297
  end
298

299 300 301 302
  def for_project_snippet?
    noteable_type == "Snippet"
  end

Riyad Preukschas's avatar
Riyad Preukschas committed
303 304 305
  # override to return commits, which are not active record
  def noteable
    if for_commit?
306
      project.commit(commit_id)
307
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
308
      super
309
    end
310 311
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
312
  rescue
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
313
    nil
314
  end
315

316
  # Mentionable override.
317 318
  def gfm_reference(from_project = nil)
    noteable.gfm_reference(from_project)
319 320 321 322 323 324 325
  end

  # Mentionable override.
  def local_reference
    noteable
  end

326
  def noteable_type_name
327
    noteable_type.downcase if noteable_type.present?
328
  end
Andrew8xx8's avatar
Andrew8xx8 committed
329 330

  # FIXME: Hack for polymorphic associations with STI
Steven Burgart's avatar
Steven Burgart committed
331
  #        For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
332 333
  def noteable_type=(noteable_type)
    super(noteable_type.to_s.classify.constantize.base_class.to_s)
Andrew8xx8's avatar
Andrew8xx8 committed
334
  end
Drew Blessing's avatar
Drew Blessing committed
335 336 337 338 339 340 341 342 343 344 345

  # Reset notes events cache
  #
  # Since we do cache @event we need to reset cache in special cases:
  # * when a note is updated
  # * when a note is removed
  # Events cache stored like  events/23-20130109142513.
  # The cache key includes updated_at timestamp.
  # Thus it will automatically generate a new fragment
  # when the event is updated because the key changes.
  def reset_events_cache
346
    Event.reset_event_cache_for(self)
Drew Blessing's avatar
Drew Blessing committed
347
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
348

349 350 351 352
  def system?
    read_attribute(:system)
  end

353
  def downvote?
354
    is_award && note == "thumbsdown"
355 356 357
  end

  def upvote?
358
    is_award && note == "thumbsup"
359 360
  end

361
  def editable?
362
    !system? && !is_award
363
  end
364

365 366 367 368
  def cross_reference_not_visible_for?(user)
    cross_reference? && referenced_mentionables(user).empty?
  end

369
  # Checks if note is an award added as a comment
370
  #
371 372
  # If note is an award, this method sets is_award to true
  #   and changes content of the note to award name.
373 374 375 376
  #
  # Method is executed as a before_validation callback.
  #
  def set_award!
377
    return unless awards_supported? && contains_emoji_only?
378 379 380 381
    self.is_award = true
    self.note = award_emoji_name
  end

382 383
  private

384 385 386 387
  def awards_supported?
    noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
  end

388
  def contains_emoji_only?
389
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
390 391 392
  end

  def award_emoji_name
393
    original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
Valery Sizov's avatar
Valery Sizov committed
394
    AwardEmoji.normilize_emoji_name(original_name)
395
  end
gitlabhq's avatar
gitlabhq committed
396
end