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

Nihad Abbasov's avatar
Nihad Abbasov committed
42
  validates :note, :project, presence: true
Valery Sizov's avatar
Valery Sizov committed
43
  validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
44
  validates :note, format: { with: /\A[-_+[:alnum:]]*\z/ }, if: -> (n){ n.is_award }
45
  validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
46
47
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
gitlabhq's avatar
gitlabhq committed
48

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

Nihad Abbasov's avatar
Nihad Abbasov committed
53
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
54
55

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

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

73
  serialize :st_diff
74
  before_create :set_diff, if: ->(n) { n.line_code.present? }
75
  before_validation :set_award!
76

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

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

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

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

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

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

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

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

130
131
132
133
  def hook_attrs
    attributes
  end

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

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

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

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

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

    false
  end

  def outdated?
    !active?
175
176
  end

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

  def diff_file_name
182
    diff.new_path if diff
183
184
  end

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

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

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

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

205
  def diff_line
206
207
    return @diff_line if @diff_line

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

    @diff_line
217
218
  end

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

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

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

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

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

    prev_lines
252
253
254
  end

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

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

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

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

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

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

  # Mentionable override.
  def local_reference
    noteable
  end

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

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

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

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

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

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

350
  def editable?
Robert Speicher's avatar
Robert Speicher committed
351
    !system?
352
  end
353
354
355
356
357
358
359
360
361
362
363

  # Checks if note is an award added from an issue comment.
  #
  # If note is an award, this method sets is_award to true,
  # and changes note content to award-emoji name.
  #
  # Awards are only supported for issue comments.
  #
  # Method is executed as a before_validation callback.
  #
  def set_award!
364
    return unless supports_awards? && contains_emoji_only?
365
366
367
368
369
370
371

    self.is_award = true
    self.note = award_emoji_name
  end

  private

372
373
374
  def supports_awards?
    noteable.kind_of?(Issue) ||
      noteable.is_a?(MergeRequest)
375
376
377
378
379
380
381
382
383
384
385
  end

  def contains_emoji_only?
    (note =~ /\A:[-_+[:alnum:]]*:\s?\z/) ? true : false
  end

  def award_emoji_name
    return nil unless contains_emoji_only?

    note.match(/\A:([-_+[:alnum:]]*):\s?/)[1]
  end
gitlabhq's avatar
gitlabhq committed
386
end