note.rb 9.14 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!

Nihad Abbasov's avatar
Nihad Abbasov committed
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, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, 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

Nihad Abbasov's avatar
Nihad Abbasov committed
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,
Yorick Peterse's avatar
Yorick Peterse committed
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