note.rb 9.6 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

40
41
  has_many :tasks, dependent: :delete_all

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

45
46
  before_validation :set_award!

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

54
55
  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
56
  validates :author, presence: true
57

Nihad Abbasov's avatar
Nihad Abbasov committed
58
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
59
60

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

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

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

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

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

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

    def grouped_awards
112
113
      notes = {}

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

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

      notes
Valery Sizov's avatar
Valery Sizov committed
122
    end
123
  end
124

125
  def cross_reference?
126
    system && SystemNoteService.cross_reference?(note)
127
128
  end

129
130
131
132
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
  end

133
  def find_diff
134
    return nil unless noteable && noteable.diffs.present?
135
136
137

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

141
142
143
144
  def hook_attrs
    attributes
  end

145
146
147
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
148
    diff = diff_for_line_code unless for_merge_request?
149
150
151
152
153
154
155
156
157
    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

158
159
160
161
  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
162
163
164
  # Check if such line of code exists in merge request diff
  # If exists - its active discussion
  # If not - its outdated diff
165
  def active?
166
    return true unless self.diff
167
    return false unless noteable
168

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

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

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

    false
  end

  def outdated?
    !active?
186
187
  end

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

  def diff_file_name
193
    diff.new_path if diff
194
195
  end

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

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

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

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

216
  def diff_line
217
218
    return @diff_line if @diff_line

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

    @diff_line
228
229
  end

230
231
232
233
234
235
236
237
238
239
240
241
242
243
  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

244
245
246
247
248
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

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

256
257
258
        break if generate_line_code(line) == self.line_code

        prev_lines.shift if prev_lines.length >= max_number_of_lines
259
260
      end
    end
261
262

    prev_lines
263
264
265
  end

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

  def highlighted_diff_lines
    Gitlab::Diff::Highlight.new(diff_lines).highlight
271
272
  end

273
  def discussion_id
274
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
275
276
277
278
279
280
281
282
283
284
285
286
287
288
  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
289
290
291
292
  def for_issue?
    noteable_type == "Issue"
  end

293
294
295
296
297
298
  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
299
  end
300

301
302
303
304
  def for_project_snippet?
    noteable_type == "Snippet"
  end

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

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

  # Mentionable override.
  def local_reference
    noteable
  end

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

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

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

351
352
353
354
  def system?
    read_attribute(:system)
  end

355
  def downvote?
356
    is_award && note == "thumbsdown"
357
358
359
  end

  def upvote?
360
    is_award && note == "thumbsup"
361
362
  end

363
  def editable?
364
    !system? && !is_award
365
  end
366

367
368
369
370
  def cross_reference_not_visible_for?(user)
    cross_reference? && referenced_mentionables(user).empty?
  end

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

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

385
386
  private

387
  def awards_supported?
388
    (noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)) && !for_diff_line?
389
390
  end

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

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