note.rb 9.64 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
#

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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"
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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!

Nihad Abbasov's avatar
Nihad Abbasov committed
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 }
Robert Speicher's avatar
Robert Speicher committed
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 }
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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

Nihad Abbasov's avatar
Nihad Abbasov committed
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) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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) }
70

71
  scope :with_associations, -> do
72
    includes(:author, :noteable, :updated_by,
Yorick Peterse's avatar
Yorick Peterse committed
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

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

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
136
137
138

    @diff ||= noteable.diffs.find do |d|
      Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
    end
  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
    diff ||= find_diff

    self.st_diff = diff.to_hash if diff
  end

152
  def diff
153
    @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
154
155
  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
  end

206
  def diff_new_line
207
    line_code.split('_')[2].to_i if line_code
Valery Sizov's avatar
Valery Sizov committed
208
  end
Cedric Gatay's avatar
Cedric Gatay committed
209

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)
Valery Sizov's avatar
Valery Sizov committed
273
  end
Cedric Gatay's avatar
Cedric Gatay committed
274

275
276
277
278
279
280
281
282
283
284
285
286
  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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
310
  # Temp fix to prevent app crash
Kevin Lyda's avatar
Kevin Lyda committed
311
  # 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

    # Responding with an emoji is not an award emoji if its on a diff comment
    return if line_code

382
383
384
385
    self.is_award = true
    self.note = award_emoji_name
  end

386
387
388
389
  private

  def awards_supported?
    noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
390
391
392
  end

  def contains_emoji_only?
393
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
394
395
396
  end

  def award_emoji_name
397
    original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
Valery Sizov's avatar
Valery Sizov committed
398
    AwardEmoji.normilize_emoji_name(original_name)
399
  end
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
400
end