note.rb 8.54 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 Mentionable
26
  include Gitlab::CurrentSettings
27
  include Participable
28

29
30
  default_value_for :system, false

31
  attr_mentionable :note
32
  participant :author, :mentioned_users
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
43
  validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
44
45
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
gitlabhq's avatar
gitlabhq committed
46

47
48
49
  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' }

Nihad Abbasov's avatar
Nihad Abbasov committed
50
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
51
52

  # Scopes
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
53
  scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
54
55
  scope :inline, ->{ where("line_code IS NOT NULL") }
  scope :not_inline, ->{ where(line_code: [nil, '']) }
56
  scope :system, ->{ where(system: true) }
57
  scope :user, ->{ where(system: false) }
58
  scope :common, ->{ where(noteable_type: ["", nil]) }
59
  scope :fresh, ->{ order(created_at: :asc, id: :asc) }
60
61
  scope :inc_author_project, ->{ includes(:project, :author) }
  scope :inc_author, ->{ includes(:author) }
62

63
  scope :with_associations, -> do
64
    includes(:author, :noteable, :updated_by,
Yorick Peterse's avatar
Yorick Peterse committed
65
             project: [:project_members, { group: [:group_members] }])
66
  end
gitlabhq's avatar
gitlabhq committed
67

68
  serialize :st_diff
69
  before_create :set_diff, if: ->(n) { n.line_code.present? }
70

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
  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
92

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

97
    def search(query)
98
      where("LOWER(note) like :query", query: "%#{query.downcase}%")
99
    end
100
  end
101

102
  def cross_reference?
103
    system && SystemNoteService.cross_reference?(note)
104
105
  end

106
107
108
109
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
  end

110
  def find_diff
111
    return nil unless noteable && noteable.diffs.present?
112
113
114

    @diff ||= noteable.diffs.find do |d|
      Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path
115
    end
116
117
  end

118
119
120
121
  def hook_attrs
    attributes
  end

122
123
124
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
125
    diff = diff_for_line_code unless for_merge_request?
126
127
128
129
130
131
132
133
134
    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

135
136
137
138
  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
139
140
141
  # Check if such line of code exists in merge request diff
  # If exists - its active discussion
  # If not - its outdated diff
142
  def active?
143
    return true unless self.diff
144
    return false unless noteable
145

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
146
147
148
    noteable.diffs.each do |mr_diff|
      next unless mr_diff.new_path == self.diff.new_path

149
      lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
150
151
152

      lines.each do |line|
        if line.text == diff_line
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
153
154
155
156
157
158
159
160
161
162
          return true
        end
      end
    end

    false
  end

  def outdated?
    !active?
163
164
  end

165
  def diff_file_index
166
    line_code.split('_')[0] if line_code
167
168
169
  end

  def diff_file_name
170
    diff.new_path if diff
171
172
  end

173
174
175
176
177
178
179
180
  def file_path
    if diff.new_path.present?
      diff.new_path
    elsif diff.old_path.present?
      diff.old_path
    end
  end

181
  def diff_old_line
182
    line_code.split('_')[1].to_i if line_code
183
184
185
  end

  def diff_new_line
186
    line_code.split('_')[2].to_i if line_code
187
188
  end

189
190
191
192
  def generate_line_code(line)
    Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
  end

193
  def diff_line
194
195
    return @diff_line if @diff_line

196
    if diff
197
      diff_lines.each do |line|
198
199
200
        if generate_line_code(line) == self.line_code
          @diff_line = line.text
        end
201
      end
202
    end
203
204

    @diff_line
205
206
  end

207
208
209
210
211
212
213
214
215
216
217
218
219
220
  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

221
222
223
224
225
226
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

    diff_lines.each do |line|
227
228
229
      if line.type == "match"
        prev_lines.clear
        prev_match_line = line
230
231
      else
        prev_lines << line
232

233
234
235
        break if generate_line_code(line) == self.line_code

        prev_lines.shift if prev_lines.length >= max_number_of_lines
236
237
      end
    end
238
239

    prev_lines
240
241
242
  end

  def diff_lines
243
    @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
244
245
  end

246
  def discussion_id
247
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
248
249
250
251
252
253
254
255
256
257
258
259
260
261
  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
262
263
264
265
  def for_issue?
    noteable_type == "Issue"
  end

266
267
268
269
270
271
  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
272
  end
273

274
275
276
277
  def for_project_snippet?
    noteable_type == "Snippet"
  end

Riyad Preukschas's avatar
Riyad Preukschas committed
278
279
280
  # override to return commits, which are not active record
  def noteable
    if for_commit?
281
      project.commit(commit_id)
282
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
283
      super
284
    end
285
286
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
287
  rescue
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
288
    nil
289
  end
290

291
292
293
294
295
296
297
298
299
300
  DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)

  # Check if the note is a downvote
  def downvote?
    votable? && note.start_with?(*DOWNVOTES)
  end

  UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)

  # Check if the note is an upvote
301
  def upvote?
302
    votable? && note.start_with?(*UPVOTES)
Riyad Preukschas's avatar
Riyad Preukschas committed
303
304
  end

305
306
  def superceded?(notes)
    return false unless vote?
307

308
309
    notes.each do |note|
      next if note == self
310

311
      if note.vote? &&
312
313
        self[:author_id] == note[:author_id] &&
        self[:created_at] <= note[:created_at]
314
315
316
        return true
      end
    end
317

318
319
320
321
322
323
324
    false
  end

  def vote?
    upvote? || downvote?
  end

Riyad Preukschas's avatar
Riyad Preukschas committed
325
326
  def votable?
    for_issue? || (for_merge_request? && !for_diff_line?)
327
  end
328

329
  # Mentionable override.
330
331
  def gfm_reference(from_project = nil)
    noteable.gfm_reference(from_project)
332
333
334
335
336
337
338
  end

  # Mentionable override.
  def local_reference
    noteable
  end

339
  def noteable_type_name
340
    noteable_type.downcase if noteable_type.present?
341
  end
Andrew8xx8's avatar
Andrew8xx8 committed
342
343

  # FIXME: Hack for polymorphic associations with STI
Steven Burgart's avatar
Steven Burgart committed
344
  #        For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
345
346
  def noteable_type=(noteable_type)
    super(noteable_type.to_s.classify.constantize.base_class.to_s)
Andrew8xx8's avatar
Andrew8xx8 committed
347
  end
Drew Blessing's avatar
Drew Blessing committed
348
349
350
351
352
353
354
355
356
357
358

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

362
363
364
365
  def system?
    read_attribute(:system)
  end

366
  def editable?
Robert Speicher's avatar
Robert Speicher committed
367
    !system?
368
  end
gitlabhq's avatar
gitlabhq committed
369
end