note.rb 7.77 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         not null
#  updated_at    :datetime         not null
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
#  st_diff       :text
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
17
#  system        :boolean          default(FALSE), not null
18
19
#

gitlabhq's avatar
gitlabhq committed
20
21
22
23
require 'carrierwave/orm/activerecord'
require 'file_size_validator'

class Note < ActiveRecord::Base
24
25
  include Mentionable

26
  attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id,
27
                  :attachment, :line_code, :commit_id
28
  attr_mentionable :note
29

gitlabhq's avatar
gitlabhq committed
30
  belongs_to :project
31
  belongs_to :noteable, polymorphic: true
Nihad Abbasov's avatar
Nihad Abbasov committed
32
  belongs_to :author, class_name: "User"
gitlabhq's avatar
gitlabhq committed
33

Nihad Abbasov's avatar
Nihad Abbasov committed
34
35
  delegate :name, to: :project, prefix: true
  delegate :name, :email, to: :author, prefix: true
36

Nihad Abbasov's avatar
Nihad Abbasov committed
37
  validates :note, :project, presence: true
38
39
  validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
  validates :attachment, file_size: { maximum: 10.megabytes.to_i }
gitlabhq's avatar
gitlabhq committed
40

41
42
43
  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
44
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
45
46

  # Scopes
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
47
  scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
48
49
  scope :inline, ->{ where("line_code IS NOT NULL") }
  scope :not_inline, ->{ where(line_code: [nil, '']) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
50

51
52
53
54
  scope :common, ->{ where(noteable_type: ["", nil]) }
  scope :fresh, ->{ order("created_at ASC, id ASC") }
  scope :inc_author_project, ->{ includes(:project, :author) }
  scope :inc_author, ->{ includes(:author) }
gitlabhq's avatar
gitlabhq committed
55

56
  serialize :st_diff
57
  before_create :set_diff, if: ->(n) { n.line_code.present? }
58

59
60
61
62
63
64
65
66
67
68
69
70
  class << self
    def create_status_change_note(noteable, project, author, status, source)
      body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_"

      create({
        noteable: noteable,
        project: project,
        author: author,
        note: body,
        system: true
      }, without_protection: true)
    end
71

72
73
74
75
76
77
78
79
80
81
82
83
84
    # +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note.
    # Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+.
    def create_cross_reference_note(noteable, mentioner, author, project)
      create({
        noteable: noteable,
        commit_id: (noteable.sha if noteable.respond_to? :sha),
        project: project,
        author: author,
        note: "_mentioned in #{mentioner.gfm_reference}_",
        system: true
      }, without_protection: true)
    end

Drew Blessing's avatar
Drew Blessing committed
85
86
87
88
89
90
91
92
93
94
95
96
    def create_assignee_change_note(noteable, project, author, assignee)
      body = assignee.nil? ? '_Assignee removed_' : "_Reassigned to @#{assignee.username}_"

      create({
        noteable: noteable,
        project: project,
        author: author,
        note: body,
        system: true
      }, without_protection: true)
    end

97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
    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
117
118
  end

119
120
121
122
123
  # Determine whether or not a cross-reference note already exists.
  def self.cross_reference_exists?(noteable, mentioner)
    where(noteable_id: noteable.id, system: true, note: "_mentioned in #{mentioner.gfm_reference}_").any?
  end

124
125
  def commit_author
    @commit_author ||=
skv's avatar
skv committed
126
127
      project.users.find_by(email: noteable.author_email) ||
      project.users.find_by(name: noteable.author_name)
128
129
  rescue
    nil
Valery Sizov's avatar
Valery Sizov committed
130
  end
Cedric Gatay's avatar
Cedric Gatay committed
131

132
  def find_diff
133
    return nil unless noteable && noteable.diffs.present?
134
135
136

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

140
141
142
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
143
    diff = Note.where(noteable_id: self.noteable_id, noteable_type: self.noteable_type, line_code: self.line_code).last.try(:diff)
144
145
146
147
148
149
150
151
152
    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

153
154
155
156
157
158
  def active?
    # TODO: determine if discussion is outdated
    # according to recent MR diff or not
    true
  end

159
  def diff_file_index
160
    line_code.split('_')[0]
161
162
163
  end

  def diff_file_name
164
    diff.new_path if diff
165
166
  end

167
168
  def diff_old_line
    line_code.split('_')[1].to_i
169
170
171
172
173
174
  end

  def diff_new_line
    line_code.split('_')[2].to_i
  end

175
  def diff_line
176
177
    return @diff_line if @diff_line

178
    if diff
179
180
181
      Gitlab::DiffParser.new(diff).each do |full_line, type, line_code, line_new, line_old|
        @diff_line = full_line if line_code == self.line_code
      end
182
    end
183
184

    @diff_line
185
186
  end

187
  def discussion_id
188
    @discussion_id ||= [:discussion, noteable_type.try(:underscore), noteable_id || commit_id, line_code].join("-").to_sym
189
190
191
192
193
  end

  # Returns true if this is a downvote note,
  # otherwise false is returned
  def downvote?
Riyad Preukschas's avatar
Riyad Preukschas committed
194
    votable? && (note.start_with?('-1') ||
195
196
                 note.start_with?(':-1:') ||
                 note.start_with?(':thumbsdown:')
197
                )
198
199
200
201
202
203
204
205
206
207
208
209
210
211
  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
212
213
214
215
  def for_issue?
    noteable_type == "Issue"
  end

216
217
218
219
220
221
  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
222
  end
223

Riyad Preukschas's avatar
Riyad Preukschas committed
224
225
226
227
  def for_wall?
    noteable_type.blank?
  end

Riyad Preukschas's avatar
Riyad Preukschas committed
228
229
230
  # override to return commits, which are not active record
  def noteable
    if for_commit?
231
      project.repository.commit(commit_id)
232
    else
Riyad Preukschas's avatar
Riyad Preukschas committed
233
      super
234
    end
235
236
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
237
  rescue
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
238
    nil
239
  end
240

241
242
243
  # Returns true if this is an upvote note,
  # otherwise false is returned
  def upvote?
Riyad Preukschas's avatar
Riyad Preukschas committed
244
    votable? && (note.start_with?('+1') ||
245
246
                 note.start_with?(':+1:') ||
                 note.start_with?(':thumbsup:')
247
                )
Riyad Preukschas's avatar
Riyad Preukschas committed
248
249
250
251
  end

  def votable?
    for_issue? || (for_merge_request? && !for_diff_line?)
252
  end
253

254
255
256
257
258
259
260
261
262
263
  # Mentionable override.
  def gfm_reference
    noteable.gfm_reference
  end

  # Mentionable override.
  def local_reference
    noteable
  end

264
265
266
267
268
269
270
  def noteable_type_name
    if noteable_type.present?
      noteable_type.downcase
    else
      "wall"
    end
  end
Andrew8xx8's avatar
Andrew8xx8 committed
271
272
273
274
275
276

  # FIXME: Hack for polymorphic associations with STI
  #        For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
  def noteable_type=(sType)
    super(sType.to_s.classify.constantize.base_class.to_s)
  end
Drew Blessing's avatar
Drew Blessing committed
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

  # 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
    Event.where(target_id: self.id, target_type: 'Note').
      order('id DESC').limit(100).
      update_all(updated_at: Time.now)
  end
gitlabhq's avatar
gitlabhq committed
292
end