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

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

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

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

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

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

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

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

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

129
130
131
132
  def hook_attrs
    attributes
  end

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

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

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

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

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

    false
  end

  def outdated?
    !active?
174
175
  end

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

  def diff_file_name
181
    diff.new_path if diff
182
183
  end

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

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

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

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

204
  def diff_line
205
206
    return @diff_line if @diff_line

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

    @diff_line
216
217
  end

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

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

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

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

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

    prev_lines
251
252
253
  end

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

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

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

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

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

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

  # Mentionable override.
  def local_reference
    noteable
  end

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

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

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

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

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

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

349
  def editable?
Robert Speicher's avatar
Robert Speicher committed
350
    !system?
351
  end
gitlabhq's avatar
gitlabhq committed
352
end