note.rb 8.05 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
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 }
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets 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
Valery Sizov's avatar
Valery Sizov committed
53
54
  scope :awards, ->{ where("is_award IS TRUE") }
  scope :nonawards, ->{ where("is_award IS FALSE") }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
55
  scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
56
57
  scope :inline, ->{ where("line_code IS NOT NULL") }
  scope :not_inline, ->{ where(line_code: [nil, '']) }
58
  scope :system, ->{ where(system: true) }
59
  scope :user, ->{ where(system: false) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
60
  scope :common, ->{ where(noteable_type: ["", nil]) }
61
  scope :fresh, ->{ order(created_at: :asc, id: :asc) }
62
63
  scope :inc_author_project, ->{ includes(:project, :author) }
  scope :inc_author, ->{ includes(:author) }
64

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

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

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

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

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

    def grouped_awards
      select(:note).distinct.map do |note|
        [ note.note, where(note: note.note) ]
      end
    end
108
  end
109

110
  def cross_reference?
111
    system && SystemNoteService.cross_reference?(note)
112
113
  end

114
115
116
117
  def max_attachment_size
    current_application_settings.max_attachment_size.megabytes.to_i
  end

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

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

126
127
128
129
  def hook_attrs
    attributes
  end

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

    self.st_diff = diff.to_hash if diff
  end

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

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

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

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

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

    false
  end

  def outdated?
    !active?
171
172
  end

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

  def diff_file_name
178
    diff.new_path if diff
179
180
  end

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

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

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

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

201
  def diff_line
202
203
    return @diff_line if @diff_line

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

    @diff_line
213
214
  end

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

229
230
231
232
233
234
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

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

241
242
243
        break if generate_line_code(line) == self.line_code

        prev_lines.shift if prev_lines.length >= max_number_of_lines
244
245
      end
    end
246
247

    prev_lines
248
249
250
  end

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

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

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

274
275
276
277
278
279
  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
280
  end
281

282
283
284
285
  def for_project_snippet?
    noteable_type == "Snippet"
  end

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

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

  # Mentionable override.
  def local_reference
    noteable
  end

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

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

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

332
333
334
335
  def system?
    read_attribute(:system)
  end

336
  def editable?
Robert Speicher's avatar
Robert Speicher committed
337
    !system?
338
  end
Dmitriy Zaporozhets's avatar
v1.0  
Dmitriy Zaporozhets committed
339
end