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

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 }
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

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) }
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,
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
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
Valery Sizov's avatar
Valery Sizov committed
110
      awards.select(:note).distinct.map do |note|
Valery Sizov's avatar
Valery Sizov committed
111
112
113
        [ note.note, where(note: note.note) ]
      end
    end
114
  end
115

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

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

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

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

132
133
134
135
  def hook_attrs
    attributes
  end

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

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

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

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

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

    false
  end

  def outdated?
    !active?
177
178
  end

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

  def diff_file_name
184
    diff.new_path if diff
185
186
  end

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

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

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

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

207
  def diff_line
208
209
    return @diff_line if @diff_line

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

    @diff_line
219
220
  end

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

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

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

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

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

    prev_lines
254
255
256
  end

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

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

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

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

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

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

  # Mentionable override.
  def local_reference
    noteable
  end

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

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

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

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

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

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

352
  def editable?
353
    !system? && !is_award
354
  end
355

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

369
370
  private

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

375
  def contains_emoji_only?
376
    note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
377
378
379
  end

  def award_emoji_name
380
    original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
Valery Sizov's avatar
Valery Sizov committed
381
    AwardEmoji.normilize_emoji_name(original_name)
382
  end
gitlabhq's avatar
gitlabhq committed
383
end