note.rb 9.11 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
19 20
#

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

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

29 30
  default_value_for :system, false

31
  attr_mentionable :note
32
  participant :author
33

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

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

42 43
  before_validation :set_award!

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

51 52
  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
53
  validates :author, presence: true
54

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

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

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

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

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
  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
99

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

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

    def grouped_awards
Valery Sizov's avatar
Valery Sizov committed
109
      awards.select(:note).distinct.map do |note|
Valery Sizov's avatar
Valery Sizov committed
110 111 112
        [ note.note, where(note: note.note) ]
      end
    end
113
  end
114

115
  def cross_reference?
116
    system && SystemNoteService.cross_reference?(note)
117 118
  end

119 120 121 122
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
  end

123
  def find_diff
124
    return nil unless noteable && noteable.diffs.present?
125 126 127

    @diff ||= noteable.diffs.find do |d|
      Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
128
    end
129 130
  end

131 132 133 134
  def hook_attrs
    attributes
  end

135 136 137
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
138
    diff = diff_for_line_code unless for_merge_request?
139 140 141 142 143 144 145 146 147
    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

148 149 150 151
  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
152 153 154
  # Check if such line of code exists in merge request diff
  # If exists - its active discussion
  # If not - its outdated diff
155
  def active?
156
    return true unless self.diff
157
    return false unless noteable
158

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
159 160 161
    noteable.diffs.each do |mr_diff|
      next unless mr_diff.new_path == self.diff.new_path

162
      lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
163 164 165

      lines.each do |line|
        if line.text == diff_line
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
166 167 168 169 170 171 172 173 174 175
          return true
        end
      end
    end

    false
  end

  def outdated?
    !active?
176 177
  end

178
  def diff_file_index
179
    line_code.split('_')[0] if line_code
180 181 182
  end

  def diff_file_name
183
    diff.new_path if diff
184 185
  end

186 187 188 189 190 191 192 193
  def file_path
    if diff.new_path.present?
      diff.new_path
    elsif diff.old_path.present?
      diff.old_path
    end
  end

194
  def diff_old_line
195
    line_code.split('_')[1].to_i if line_code
196 197 198
  end

  def diff_new_line
199
    line_code.split('_')[2].to_i if line_code
200 201
  end

202 203 204 205
  def generate_line_code(line)
    Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
  end

206
  def diff_line
207 208
    return @diff_line if @diff_line

209
    if diff
210
      diff_lines.each do |line|
211 212 213
        if generate_line_code(line) == self.line_code
          @diff_line = line.text
        end
214
      end
215
    end
216 217

    @diff_line
218 219
  end

220 221 222 223 224 225 226 227 228 229 230 231 232 233
  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

234 235 236 237 238 239
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

    diff_lines.each do |line|
240 241 242
      if line.type == "match"
        prev_lines.clear
        prev_match_line = line
243 244
      else
        prev_lines << line
245

246 247 248
        break if generate_line_code(line) == self.line_code

        prev_lines.shift if prev_lines.length >= max_number_of_lines
249 250
      end
    end
251 252

    prev_lines
253 254 255
  end

  def diff_lines
256
    @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
257 258
  end

259
  def discussion_id
260
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
261 262 263 264 265 266 267 268 269 270 271 272 273 274
  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
275 276 277 278
  def for_issue?
    noteable_type == "Issue"
  end

279 280 281 282 283 284
  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
285
  end
286

287 288 289 290
  def for_project_snippet?
    noteable_type == "Snippet"
  end

Riyad Preukschas's avatar
Riyad Preukschas committed
291 292 293
  # override to return commits, which are not active record
  def noteable
    if for_commit?
294
      project.commit(commit_id)
295
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
296
      super
297
    end
298 299
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
300
  rescue
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
301
    nil
302
  end
303

304
  # Mentionable override.
305 306
  def gfm_reference(from_project = nil)
    noteable.gfm_reference(from_project)
307 308 309 310 311 312 313
  end

  # Mentionable override.
  def local_reference
    noteable
  end

314
  def noteable_type_name
315
    noteable_type.downcase if noteable_type.present?
316
  end
Andrew8xx8's avatar
Andrew8xx8 committed
317 318

  # FIXME: Hack for polymorphic associations with STI
Steven Burgart's avatar
Steven Burgart committed
319
  #        For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
320 321
  def noteable_type=(noteable_type)
    super(noteable_type.to_s.classify.constantize.base_class.to_s)
Andrew8xx8's avatar
Andrew8xx8 committed
322
  end
Drew Blessing's avatar
Drew Blessing committed
323 324 325 326 327 328 329 330 331 332 333

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

337 338 339 340
  def system?
    read_attribute(:system)
  end

341 342 343 344 345 346 347 348 349 350
  # Deprecated. Still exists to preserve API compatibility.
  def downvote?
    false
  end

  # Deprecated. Still exists to preserve API compatibility.
  def upvote?
    false
  end

351
  def editable?
Robert Speicher's avatar
Robert Speicher committed
352
    !system?
353
  end
354

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

368 369
  private

370 371 372 373
  def awards_supported?
    noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
  end

374
  def contains_emoji_only?
375
    note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/
376 377 378
  end

  def award_emoji_name
379
    note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1]
380
  end
gitlabhq's avatar
gitlabhq committed
381
end