note.rb 9.32 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
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 247
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

    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
    @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
265 266
  end

267
  def discussion_id
268
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
269 270 271 272 273 274 275 276 277 278 279 280 281 282
  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
283 284 285 286
  def for_issue?
    noteable_type == "Issue"
  end

287 288 289 290 291 292
  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
293
  end
294

295 296 297 298
  def for_project_snippet?
    noteable_type == "Snippet"
  end

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

312
  # Mentionable override.
313 314
  def gfm_reference(from_project = nil)
    noteable.gfm_reference(from_project)
315 316 317 318 319 320 321
  end

  # Mentionable override.
  def local_reference
    noteable
  end

322
  def noteable_type_name
323
    noteable_type.downcase if noteable_type.present?
324
  end
Andrew8xx8's avatar
Andrew8xx8 committed
325 326

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

  # 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
342
    Event.reset_event_cache_for(self)
Drew Blessing's avatar
Drew Blessing committed
343
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
344

345 346 347 348
  def system?
    read_attribute(:system)
  end

349
  def downvote?
350
    is_award && note == "thumbsdown"
351 352 353
  end

  def upvote?
354
    is_award && note == "thumbsup"
355 356
  end

357
  def editable?
358
    !system? && !is_award
359
  end
360

361
  # Checks if note is an award added as a comment
362
  #
363 364
  # If note is an award, this method sets is_award to true
  #   and changes content of the note to award name.
365 366 367 368
  #
  # Method is executed as a before_validation callback.
  #
  def set_award!
369
    return unless awards_supported? && contains_emoji_only?
370 371 372 373
    self.is_award = true
    self.note = award_emoji_name
  end

374 375
  private

376 377 378 379
  def awards_supported?
    noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
  end

380
  def contains_emoji_only?
381
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
382 383 384
  end

  def award_emoji_name
385
    original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
Valery Sizov's avatar
Valery Sizov committed
386
    AwardEmoji.normilize_emoji_name(original_name)
387
  end
gitlabhq's avatar
gitlabhq committed
388
end