note.rb 8.37 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
#

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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

Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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"
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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 :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
45
46
  # Attachments are deprecated and are handled by Markdown uploader
  validates :attachment, file_size: { maximum: :max_attachment_size }
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
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' }
Valery Sizov's avatar
Valery Sizov committed
50
  validates :author, presence: true
51

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

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

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

72
  serialize :st_diff
73
  before_create :set_diff, if: ->(n) { n.line_code.present? }
74

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

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

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

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

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

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

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

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

128
129
130
131
  def hook_attrs
    attributes
  end

132
133
134
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
135
    diff = diff_for_line_code unless for_merge_request?
136
137
138
139
140
    diff ||= find_diff

    self.st_diff = diff.to_hash if diff
  end

141
  def diff
142
    @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
143
144
  end

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

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

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

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

    false
  end

  def outdated?
    !active?
173
174
  end

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

  def diff_file_name
180
    diff.new_path if diff
181
182
  end

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

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

195
  def diff_new_line
196
    line_code.split('_')[2].to_i if line_code
Valery Sizov's avatar
Valery Sizov committed
197
  end
Cedric Gatay's avatar
Cedric Gatay committed
198

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

203
  def diff_line
204
205
    return @diff_line if @diff_line

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

    @diff_line
215
216
  end

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

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

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

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

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

    prev_lines
250
251
252
  end

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

256
  def discussion_id
257
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
Valery Sizov's avatar
Valery Sizov committed
258
  end
Cedric Gatay's avatar
Cedric Gatay committed
259

260
261
262
263
264
265
266
267
268
269
270
271
  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
272
273
274
275
  def for_issue?
    noteable_type == "Issue"
  end

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

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

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

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

  # Mentionable override.
  def local_reference
    noteable
  end

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

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

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

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

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

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

348
  def editable?
Robert Speicher's avatar
Robert Speicher committed
349
    !system?
350
  end
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
351
end