BigW Consortium Gitlab

note.rb 14.3 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 committed
9 10
#  created_at    :datetime
#  updated_at    :datetime
11 12 13
#  project_id    :integer
#  attachment    :string(255)
#  line_code     :string(255)
14 15
#  commit_id     :string(255)
#  noteable_id   :integer
Dmitriy Zaporozhets committed
16
#  system        :boolean          default(FALSE), not null
Dmitriy Zaporozhets committed
17
#  st_diff       :text
18 19
#

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

class Note < ActiveRecord::Base
24 25
  include Mentionable

26 27
  default_value_for :system, false

28
  attr_mentionable :note
29

gitlabhq committed
30
  belongs_to :project
31
  belongs_to :noteable, polymorphic: true
32
  belongs_to :author, class_name: "User"
gitlabhq committed
33

34 35
  delegate :name, to: :project, prefix: true
  delegate :name, :email, to: :author, prefix: true
36

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 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' }

44
  mount_uploader :attachment, AttachmentUploader
Andrey Kumanyaev committed
45 46

  # Scopes
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, '']) }
50
  scope :system, ->{ where(system: true) }
51
  scope :common, ->{ where(noteable_type: ["", nil]) }
52
  scope :fresh, ->{ order(created_at: :asc, id: :asc) }
53 54
  scope :inc_author_project, ->{ includes(:project, :author) }
  scope :inc_author, ->{ includes(:author) }
gitlabhq committed
55

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

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

64
      create(
65 66 67 68 69
        noteable: noteable,
        project: project,
        author: author,
        note: body,
        system: true
70
      )
71
    end
72

73 74 75 76
    # +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+.
77
    def create_cross_reference_note(noteable, mentioner, author, project)
78 79
      gfm_reference = mentioner_gfm_ref(noteable, mentioner, project)

80
      note_options = {
81 82
        project: project,
        author: author,
83
        note: cross_reference_note_content(gfm_reference),
84
        system: true
85 86 87 88 89 90 91 92
      }

      if noteable.kind_of?(Commit)
        note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
      else
        note_options.merge!(noteable: noteable)
      end

93
      create(note_options) unless cross_reference_disallowed?(noteable, mentioner)
94 95
    end

96 97 98 99 100 101 102
    def create_milestone_change_note(noteable, project, author, milestone)
      body = if milestone.nil?
               '_Milestone removed_'
             else
               "_Milestone changed to #{milestone.title}_"
             end

103
      create(
104 105 106 107 108
        noteable: noteable,
        project: project,
        author: author,
        note: body,
        system: true
109
      )
110 111
    end

112 113 114 115 116 117 118 119 120
    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
121
      })
122 123
    end

Nikita Verkhovin committed
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
    def create_labels_change_note(noteable, project, author, added_labels, removed_labels)
      labels_count = added_labels.count + removed_labels.count
      added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ')
      removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ')
      message = ''

      if added_labels.present?
        message << "added #{added_labels}"
      end

      if added_labels.present? && removed_labels.present?
        message << ' and '
      end

      if removed_labels.present?
        message << "removed #{removed_labels}"
      end

      message << ' ' << 'label'.pluralize(labels_count)
      body = "_#{message.capitalize}_"

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

154
    def create_new_commits_note(noteable, project, author, commits)
155 156
      commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit')
      body = "Added #{commits_text}:\n\n"
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

      commits.each do |commit|
        message = "* #{commit.short_id} - #{commit.title}"
        body << message
        body << "\n"
      end

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

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    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
193

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

198 199 200 201 202 203 204 205 206
    # Determine if cross reference note should be created.
    # eg. mentioning a commit in MR comments which exists inside a MR
    # should not create "mentioned in" note.
    def cross_reference_disallowed?(noteable, mentioner)
      if mentioner.kind_of?(MergeRequest)
        mentioner.commits.map(&:id).include? noteable.id
      end
    end

207 208
    # Determine whether or not a cross-reference note already exists.
    def cross_reference_exists?(noteable, mentioner)
209
      gfm_reference = mentioner_gfm_ref(noteable, mentioner)
210 211 212 213 214 215
      notes = if noteable.is_a?(Commit)
                where(commit_id: noteable.id)
              else
                where(noteable_id: noteable.id)
              end

216
      notes.where('note like ?', cross_reference_note_content(gfm_reference)).
217
        system.any?
218
    end
219 220 221 222

    def search(query)
      where("note like :query", query: "%#{query}%")
    end
223

224 225 226 227
    def cross_reference_note_prefix
      '_mentioned in '
    end

228 229
    private

230 231 232 233
    def cross_reference_note_content(gfm_reference)
      cross_reference_note_prefix + "#{gfm_reference}_"
    end

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
    # Prepend the mentioner's namespaced project path to the GFM reference for
    # cross-project references.  For same-project references, return the
    # unmodified GFM reference.
    def mentioner_gfm_ref(noteable, mentioner, project = nil)
      if mentioner.is_a?(Commit)
        if project.nil?
          return mentioner.gfm_reference.sub('commit ', 'commit %')
        else
          mentioning_project = project
        end
      else
        mentioning_project = mentioner.project
      end

      noteable_project_id = noteable_project_id(noteable, mentioning_project)

      full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
    end

    # Return the ID of the project that +noteable+ belongs to, or nil if
    # +noteable+ is a commit and is not part of the project that owns
    # +mentioner+.
    def noteable_project_id(noteable, mentioning_project)
      if noteable.is_a?(Commit)
        if mentioning_project.repository.commit(noteable.id)
          # The noteable commit belongs to the mentioner's project
          mentioning_project.id
        else
          nil
        end
      else
        noteable.project.id
      end
    end

    # Return the +mentioner+ GFM reference.  If the mentioner and noteable
    # projects are not the same, add the mentioning project's path to the
    # returned value.
    def full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
      if mentioning_project.id == noteable_project_id
        mentioner.gfm_reference
      else
        if mentioner.is_a?(Commit)
          mentioner.gfm_reference.sub(
            /(commit )/,
            "\\1#{mentioning_project.path_with_namespace}@"
          )
        else
          mentioner.gfm_reference.sub(
            /(issue |merge request )/,
            "\\1#{mentioning_project.path_with_namespace}"
          )
        end
      end
    end
289 290
  end

291 292
  def commit_author
    @commit_author ||=
293 294
      project.team.users.find_by(email: noteable.author_email) ||
      project.team.users.find_by(name: noteable.author_name)
295 296
  rescue
    nil
Valery Sizov committed
297
  end
298

299 300 301 302
  def cross_reference?
    note.start_with?(self.class.cross_reference_note_prefix)
  end

303
  def find_diff
304
    return nil unless noteable && noteable.diffs.present?
305 306 307

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

311 312 313
  def set_diff
    # First lets find notes with same diff
    # before iterating over all mr diffs
314
    diff = Note.where(noteable_id: self.noteable_id, noteable_type: self.noteable_type, line_code: self.line_code).last.try(:diff)
315 316 317 318 319 320 321 322 323
    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

324 325 326
  # Check if such line of code exists in merge request diff
  # If exists - its active discussion
  # If not - its outdated diff
327
  def active?
328
    return true unless self.diff
329
    return false unless noteable
330

331 332 333
    noteable.diffs.each do |mr_diff|
      next unless mr_diff.new_path == self.diff.new_path

334
      lines = Gitlab::Diff::Parser.new.parse(mr_diff.diff.lines.to_a)
335 336 337

      lines.each do |line|
        if line.text == diff_line
338 339 340 341 342 343 344 345 346 347
          return true
        end
      end
    end

    false
  end

  def outdated?
    !active?
348 349
  end

350
  def diff_file_index
351
    line_code.split('_')[0] if line_code
352 353 354
  end

  def diff_file_name
355
    diff.new_path if diff
356 357
  end

358 359 360 361 362 363 364 365
  def file_path
    if diff.new_path.present?
      diff.new_path
    elsif diff.old_path.present?
      diff.old_path
    end
  end

366
  def diff_old_line
367
    line_code.split('_')[1].to_i if line_code
368 369 370
  end

  def diff_new_line
371
    line_code.split('_')[2].to_i if line_code
372 373
  end

374 375 376 377
  def generate_line_code(line)
    Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
  end

378
  def diff_line
379 380
    return @diff_line if @diff_line

381
    if diff
382
      diff_lines.each do |line|
383 384 385
        if generate_line_code(line) == self.line_code
          @diff_line = line.text
        end
386
      end
387
    end
388 389

    @diff_line
390 391
  end

392 393 394 395 396 397 398 399 400 401 402 403 404 405
  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

406 407 408 409 410 411
  def truncated_diff_lines
    max_number_of_lines = 16
    prev_match_line = nil
    prev_lines = []

    diff_lines.each do |line|
412 413 414
      if line.type == "match"
        prev_lines.clear
        prev_match_line = line
415 416
      else
        prev_lines << line
417 418 419 420
        
        break if generate_line_code(line) == self.line_code

        prev_lines.shift if prev_lines.length >= max_number_of_lines
421 422
      end
    end
423 424

    prev_lines
425 426 427
  end

  def diff_lines
428
    @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.lines.to_a)
429 430
  end

431
  def discussion_id
432
    @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
433 434 435 436 437
  end

  # Returns true if this is a downvote note,
  # otherwise false is returned
  def downvote?
Riyad Preukschas committed
438
    votable? && (note.start_with?('-1') ||
439
                 note.start_with?(':-1:') ||
440 441
                 note.start_with?(':thumbsdown:') ||
                 note.start_with?(':thumbs_down_sign:')
442
                )
443 444 445 446 447 448 449 450 451 452 453 454 455 456
  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 committed
457 458 459 460
  def for_issue?
    noteable_type == "Issue"
  end

461 462 463 464 465 466
  def for_merge_request?
    noteable_type == "MergeRequest"
  end

  def for_merge_request_diff_line?
    for_merge_request? && for_diff_line?
467
  end
468

469 470 471
  # override to return commits, which are not active record
  def noteable
    if for_commit?
472
      project.repository.commit(commit_id)
473
    else
474
      super
475
    end
476 477
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
478
  rescue
479
    nil
480
  end
481

482 483 484
  # Returns true if this is an upvote note,
  # otherwise false is returned
  def upvote?
Riyad Preukschas committed
485
    votable? && (note.start_with?('+1') ||
486
                 note.start_with?(':+1:') ||
487 488
                 note.start_with?(':thumbsup:') ||
                 note.start_with?(':thumbs_up_sign:')
489
                )
Riyad Preukschas committed
490 491
  end

492 493
  def superceded?(notes)
    return false unless vote?
494

495 496
    notes.each do |note|
      next if note == self
497

498
      if note.vote? &&
499 500
        self[:author_id] == note[:author_id] &&
        self[:created_at] <= note[:created_at]
501 502 503
        return true
      end
    end
504

505 506 507 508 509 510 511
    false
  end

  def vote?
    upvote? || downvote?
  end

Riyad Preukschas committed
512 513
  def votable?
    for_issue? || (for_merge_request? && !for_diff_line?)
514
  end
515

516 517 518 519 520 521 522 523 524 525
  # Mentionable override.
  def gfm_reference
    noteable.gfm_reference
  end

  # Mentionable override.
  def local_reference
    noteable
  end

526 527 528 529 530
  def noteable_type_name
    if noteable_type.present?
      noteable_type.downcase
    end
  end
Andrew8xx8 committed
531 532

  # FIXME: Hack for polymorphic associations with STI
Steven Burgart committed
533
  #        For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations
Andrew8xx8 committed
534 535 536
  def noteable_type=(sType)
    super(sType.to_s.classify.constantize.base_class.to_s)
  end
537 538 539 540 541 542 543 544 545 546 547

  # 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
548
    Event.reset_event_cache_for(self)
549
  end
550 551 552 553

  def set_references
    notice_added_references(project, author)
  end
554 555

  def editable?
556
    !read_attribute(:system)
557
  end
gitlabhq committed
558
end