Class: Note

Overview

A note on the root of an issue, merge request, commit, or snippet.

A note of this type is never resolvable.

Constant Summary collapse

TYPES_RESTRICTED_BY_PROJECT_ABILITY =
{
  branch: :download_code
}.freeze
TYPES_RESTRICTED_BY_GROUP_ABILITY =
{
  contact: :read_crm_contact
}.freeze
NON_DIFF_NOTE_TYPES =
['Note', 'DiscussionNote', nil].freeze

Constants included from ThrottledTouch

ThrottledTouch::TOUCH_INTERVAL

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_WORD

Constants included from ResolvableNote

ResolvableNote::RESOLVABLE_TYPES

Constants included from CacheMarkdownField

CacheMarkdownField::INVALIDATED_BY

Constants included from Redactable

Redactable::UNSUBSCRIBE_PATTERN

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Instance Attribute Summary collapse

Attributes included from Importable

#imported, #importing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from Editable

#last_edited_by

Methods included from ResolvableNote

#potentially_resolvable?, #resolvable?, #resolve!, #resolve_without_save, #resolved?, #to_be_resolved?, #unresolve!, #unresolve_without_save

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from CacheMarkdownField

#attribute_invalidated?, #cached_html_for, #cached_html_up_to_date?, #can_cache_field?, #invalidated_markdown_cache?, #latest_cached_markdown_version, #local_version, #mentionable_attributes_changed?, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #store_mentions!, #updated_cached_html_for

Methods included from FasterCacheKeys

#cache_key

Methods included from Awardable

#awarded_emoji?, #downvotes, #grouped_awards, #upvotes, #user_authored?, #user_can_award?

Methods included from Mentionable

#all_references, #create_cross_references!, #create_new_cross_references!, #directly_addressed_users, #extractors, #gfm_reference, #local_reference, #matches_cross_reference_regex?, #mentioned_users, #referenced_group_users, #referenced_groups, #referenced_mentionables, #referenced_project_users, #referenced_projects, #referenced_users

Methods included from Participable

#participant?, #participants, #visible_participants

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods inherited from ApplicationRecord

cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from SensitiveSerializableHash

#serializable_hash

Instance Attribute Details

#commands_changesObject


634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# File 'app/models/note.rb', line 634

def commands_changes
  @commands_changes&.slice(
    :due_date,
    :label_ids,
    :remove_label_ids,
    :add_label_ids,
    :canonical_issue_id,
    :clone_with_notes,
    :confidential,
    :create_merge_request,
    :add_contacts,
    :remove_contacts,
    :assignee_ids,
    :milestone_id,
    :time_estimate,
    :spend_time,
    :discussion_locked,
    :merge,
    :rebase,
    :wip_event,
    :target_branch,
    :reviewer_ids,
    :health_status,
    :promote_to_epic,
    :weight,
    :emoji_award,
    :todo_event,
    :subscription_event,
    :state_event,
    :title,
    :tag_message,
    :tag_name
  )
end

#redacted_note_htmlObject

Attribute containing rendered and redacted Markdown as generated by Banzai::ObjectRenderer.


46
47
48
# File 'app/models/note.rb', line 46

def redacted_note_html
  @redacted_note_html
end

#skip_keep_around_commitsObject

Attribute used to determine whether keep_around_commits will be skipped for diff notes.


58
59
60
# File 'app/models/note.rb', line 58

def skip_keep_around_commits
  @skip_keep_around_commits
end

#total_reference_countObject

Total of all references as generated by Banzai::ObjectRenderer


49
50
51
# File 'app/models/note.rb', line 49

def total_reference_count
  @total_reference_count
end

#user_visible_reference_countObject

Number of user visible references as generated by Banzai::ObjectRenderer


52
53
54
# File 'app/models/note.rb', line 52

def user_visible_reference_count
  @user_visible_reference_count
end

Class Method Details

.cherry_picked_merge_requests(shas) ⇒ Object


239
240
241
# File 'app/models/note.rb', line 239

def cherry_picked_merge_requests(shas)
  where(noteable_type: 'MergeRequest', commit_id: shas).select(:noteable_id)
end

.count_for_collection(ids, type, count_column = 'COUNT(*) as count') ⇒ Object


222
223
224
225
226
# File 'app/models/note.rb', line 222

def count_for_collection(ids, type, count_column = 'COUNT(*) as count')
  user.select(:noteable_id, count_column)
    .group(:noteable_id)
    .where(noteable_type: type, noteable_id: ids)
end

.discussions(context_noteable = nil) ⇒ Object


179
180
181
# File 'app/models/note.rb', line 179

def discussions(context_noteable = nil)
  Discussion.build_collection(all.includes(:noteable).fresh, context_noteable)
end

.find_discussion(discussion_id) ⇒ Object

Note: Where possible consider using Discussion#lazy_find to return Discussions in order to benefit from having records batch loaded.


185
186
187
188
189
190
191
# File 'app/models/note.rb', line 185

def find_discussion(discussion_id)
  notes = where(discussion_id: discussion_id).fresh.to_a

  return if notes.empty?

  Discussion.build(notes)
end

.grouped_diff_discussions(diff_refs = nil) ⇒ Object

Group diff discussions by line code or file path. It is not needed to group by line code when comment is on an image.


196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'app/models/note.rb', line 196

def grouped_diff_discussions(diff_refs = nil)
  groups = {}

  diff_notes.fresh.discussions.each do |discussion|
    group_key =
      if discussion.on_image?
        discussion.file_new_path
      else
        discussion.line_code_in_diffs(diff_refs)
      end

    if group_key
      discussions = groups[group_key] ||= []
      discussions << discussion
    end
  end

  groups
end

.model_nameObject


175
176
177
# File 'app/models/note.rb', line 175

def model_name
  ActiveModel::Name.new(self, nil, 'note')
end

.positionsObject


216
217
218
219
220
# File 'app/models/note.rb', line 216

def positions
  where.not(position: nil)
    .select(:id, :type, :position) # ActiveRecord needs id and type for typecasting.
    .map(&:position)
end

.search(query) ⇒ Object


228
229
230
# File 'app/models/note.rb', line 228

def search(query)
  fuzzy_search(query, [:note])
end

.simple_sortsObject


235
236
237
# File 'app/models/note.rb', line 235

def simple_sorts
  super.except('name_asc', 'name_desc')
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)

260
261
262
# File 'app/models/note.rb', line 260

def active?
  true
end

#award_emoji?Boolean

Returns:

  • (Boolean)

402
403
404
# File 'app/models/note.rb', line 402

def award_emoji?
  can_be_award_emoji? && contains_emoji_only?
end

#banzai_render_context(field) ⇒ Object


570
571
572
# File 'app/models/note.rb', line 570

def banzai_render_context(field)
  super.merge(noteable: noteable, system_note: system?, label_url_method: noteable_label_url_method)
end

#bump_updated_atObject


508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'app/models/note.rb', line 508

def bump_updated_at
  # Instead of calling touch which is throttled via ThrottledTouch concern,
  # we bump the updated_at column directly. This also prevents executing
  # after_commit callbacks that we don't need.
  attributes_to_update = { updated_at: Time.current }

  # Notes that were edited before the `last_edited_at` column was added, fall back to `updated_at` for the edit time.
  # We copy this over to the correct column so we don't erroneously change the edit timestamp.
  if updated_by_id.present? && read_attribute(:last_edited_at).blank?
    attributes_to_update[:last_edited_at] = updated_at
  end

  update_columns(attributes_to_update)
end

#can_be_award_emoji?Boolean

Returns:

  • (Boolean)

410
411
412
# File 'app/models/note.rb', line 410

def can_be_award_emoji?
  noteable.is_a?(Awardable) && !part_of_discussion?
end

#can_be_discussion_note?Boolean

Returns:

  • (Boolean)

430
431
432
# File 'app/models/note.rb', line 430

def can_be_discussion_note?
  self.noteable.supports_discussions? && !part_of_discussion?
end

#can_create_todo?Boolean

Returns:

  • (Boolean)

434
435
436
437
# File 'app/models/note.rb', line 434

def can_create_todo?
  # Skip system notes, and notes on snippets
  !system? && !for_snippet?
end

#commitObject


324
325
326
# File 'app/models/note.rb', line 324

def commit
  @commit ||= project.commit(commit_id) if commit_id.present?
end

#confidential?(include_noteable: false) ⇒ Boolean

Returns:

  • (Boolean)

378
379
380
381
382
# File 'app/models/note.rb', line 378

def confidential?(include_noteable: false)
  return true if confidential

  include_noteable && noteable.try(:confidential?)
end

#contains_emoji_only?Boolean

Returns:

  • (Boolean)

414
415
416
# File 'app/models/note.rb', line 414

def contains_emoji_only?
  note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
end

#contributor?Boolean

Returns:

  • (Boolean)

366
367
368
# File 'app/models/note.rb', line 366

def contributor?
  project&.team&.contributor?(self.author_id)
end

#diff_note?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)

256
257
258
# File 'app/models/note.rb', line 256

def diff_note?
  false
end

#discussionObject

Returns the entire discussion this note is part of. Consider using `#to_discussion` if we do not need to render the discussion and all its notes and if we don't care about the discussion's resolvability status.


466
467
468
469
470
471
# File 'app/models/note.rb', line 466

def discussion
  strong_memoize(:discussion) do
    full_discussion = self.noteable.notes.find_discussion(self.discussion_id) if self.noteable && part_of_discussion?
    full_discussion || to_discussion
  end
end

#discussion_class(noteable = nil) ⇒ Object


439
440
441
442
443
444
445
446
447
448
# File 'app/models/note.rb', line 439

def discussion_class(noteable = nil)
  # When commit notes are rendered on an MR's Discussion page, they are
  # displayed in one discussion instead of individually.
  # See also `#discussion_id` and `Discussion.override_discussion_id`.
  if noteable && noteable != self.noteable
    OutOfContextDiscussion
  else
    IndividualNoteDiscussion
  end
end

#discussion_id(noteable = nil) ⇒ Object

See `Discussion.override_discussion_id` for details.


451
452
453
# File 'app/models/note.rb', line 451

def discussion_id(noteable = nil)
  discussion_class(noteable).override_discussion_id(self) || super() || ensure_discussion_id
end

#editable?Boolean

Returns:

  • (Boolean)

384
385
386
# File 'app/models/note.rb', line 384

def editable?
  !system?
end

#edited?Boolean

Since we used `updated_at` as `last_edited_at`, it could be touched by transforming / resolving a note. This makes sure it is only marked as edited when the note body is updated.

Returns:

  • (Boolean)

396
397
398
399
400
# File 'app/models/note.rb', line 396

def edited?
  return false if updated_by.blank?

  super
end

#emoji_awardable?Boolean

Returns:

  • (Boolean)

406
407
408
# File 'app/models/note.rb', line 406

def emoji_awardable?
  !system?
end

#expire_etag_cacheObject


523
524
525
# File 'app/models/note.rb', line 523

def expire_etag_cache
  noteable&.expire_note_etag_cache
end

#for_alert_mangement_alert?Boolean

Returns:

  • (Boolean)

292
293
294
# File 'app/models/note.rb', line 292

def for_alert_mangement_alert?
  noteable_type == 'AlertManagement::Alert'
end

#for_commit?Boolean

Returns:

  • (Boolean)

276
277
278
# File 'app/models/note.rb', line 276

def for_commit?
  noteable_type == "Commit"
end

#for_design?Boolean

Returns:

  • (Boolean)

312
313
314
# File 'app/models/note.rb', line 312

def for_design?
  noteable_type == DesignManagement::Design.name
end

#for_issuable?Boolean

Returns:

  • (Boolean)

316
317
318
# File 'app/models/note.rb', line 316

def for_issuable?
  for_issue? || for_merge_request?
end

#for_issue?Boolean

Returns:

  • (Boolean)

280
281
282
# File 'app/models/note.rb', line 280

def for_issue?
  noteable_type == "Issue"
end

#for_merge_request?Boolean

Returns:

  • (Boolean)

284
285
286
# File 'app/models/note.rb', line 284

def for_merge_request?
  noteable_type == "MergeRequest"
end

#for_personal_snippet?Boolean

Returns:

  • (Boolean)

304
305
306
# File 'app/models/note.rb', line 304

def for_personal_snippet?
  noteable.is_a?(PersonalSnippet)
end

#for_project_noteable?Boolean

Returns:

  • (Boolean)

308
309
310
# File 'app/models/note.rb', line 308

def for_project_noteable?
  !for_personal_snippet?
end

#for_project_snippet?Boolean

Returns:

  • (Boolean)

300
301
302
# File 'app/models/note.rb', line 300

def for_project_snippet?
  noteable.is_a?(ProjectSnippet)
end

#for_snippet?Boolean

Returns:

  • (Boolean)

288
289
290
# File 'app/models/note.rb', line 288

def for_snippet?
  noteable_type == "Snippet"
end

#for_vulnerability?Boolean

Returns:

  • (Boolean)

296
297
298
# File 'app/models/note.rb', line 296

def for_vulnerability?
  noteable_type == "Vulnerability"
end

#hook_attrsObject


268
269
270
# File 'app/models/note.rb', line 268

def hook_attrs
  Gitlab::HookData::NoteBuilder.new(self).build
end

#in_reply_to?(other) ⇒ Boolean

Returns:

  • (Boolean)

481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'app/models/note.rb', line 481

def in_reply_to?(other)
  case other
  when Note
    if part_of_discussion?
      in_reply_to?(other.noteable) && in_reply_to?(other.to_discussion)
    else
      in_reply_to?(other.noteable)
    end
  when Discussion
    self.discussion_id == other.id
  when Noteable
    self.noteable == other
  else
    false
  end
end

#last_edited_atObject

We used `last_edited_at` as an alias of `updated_at` before. This makes it compatible with the previous way without data migration.


390
391
392
# File 'app/models/note.rb', line 390

def last_edited_at
  super || updated_at
end

#max_attachment_sizeObject


264
265
266
# File 'app/models/note.rb', line 264

def max_attachment_size
  Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end

#merge_requestsObject

Notes on merge requests and commits can be traced back to one or several MRs. This method returns a relation if the note is for one of these types, or nil if it is a note on some other object.


331
332
333
334
335
336
337
338
339
# File 'app/models/note.rb', line 331

def merge_requests
  if for_commit?
    project.merge_requests.by_commit_sha(commit_id)
  elsif for_merge_request?
    MergeRequest.id_in(noteable_id)
  else
    nil
  end
end

#noteableObject

override to return commits, which are not active record


342
343
344
345
346
347
348
349
350
# File 'app/models/note.rb', line 342

def noteable
  return commit if for_commit?

  super
rescue StandardError
  # Temp fix to prevent app crash
  # if note commit id doesn't exist
  nil
end

#noteable_ability_nameObject


418
419
420
421
422
423
424
425
426
427
428
# File 'app/models/note.rb', line 418

def noteable_ability_name
  if for_snippet?
    'snippet'
  elsif for_alert_mangement_alert?
    'alert_management_alert'
  elsif for_vulnerability?
    'security_resource'
  else
    noteable_type.demodulize.underscore
  end
end

#noteable_assignee_or_author?(user) ⇒ Boolean

Returns:

  • (Boolean)

358
359
360
361
362
363
364
# File 'app/models/note.rb', line 358

def noteable_assignee_or_author?(user)
  return false unless user
  return false unless noteable.respond_to?(:author_id)
  return noteable.assignee_or_author?(user) if [MergeRequest, Issue].include?(noteable.class)

  noteable.author_id == user.id
end

#noteable_author?(noteable) ⇒ Boolean

Returns:

  • (Boolean)

370
371
372
# File 'app/models/note.rb', line 370

def noteable_author?(noteable)
  noteable.author == self.author
end

#noteable_type=(noteable_type) ⇒ Object

FIXME: Hack for polymorphic associations with STI

For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations

354
355
356
# File 'app/models/note.rb', line 354

def noteable_type=(noteable_type)
  super(noteable_type.to_s.classify.constantize.base_class.to_s)
end

#notify_after_createObject


562
563
564
# File 'app/models/note.rb', line 562

def notify_after_create
  noteable&.after_note_created(self)
end

#notify_after_destroyObject


566
567
568
# File 'app/models/note.rb', line 566

def notify_after_destroy
  noteable&.after_note_destroyed(self)
end

#parent_userObject


594
595
596
# File 'app/models/note.rb', line 594

def parent_user
  noteable.author if for_personal_snippet?
end

#part_of_discussion?Boolean

Returns:

  • (Boolean)

477
478
479
# File 'app/models/note.rb', line 477

def part_of_discussion?
  !to_discussion.individual_note?
end

#post_processed_cache_keyObject


602
603
604
605
606
607
608
# File 'app/models/note.rb', line 602

def post_processed_cache_key
  cache_key_items = [cache_key, author&.cache_key]
  cache_key_items << project.team.human_max_access(author&.id) if author.present?
  cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?

  cache_key_items.join(':')
end

#project_nameObject


374
375
376
# File 'app/models/note.rb', line 374

def project_name
  project&.name
end

#referencesObject


498
499
500
501
502
503
504
505
506
# File 'app/models/note.rb', line 498

def references
  refs = [noteable]

  if part_of_discussion?
    refs += discussion.notes.take_while { |n| n.id < id }
  end

  refs
end

#resource_parentObject


578
579
580
# File 'app/models/note.rb', line 578

def resource_parent
  project
end

#retrieve_upload(_identifier, paths) ⇒ Object


574
575
576
# File 'app/models/note.rb', line 574

def retrieve_upload(_identifier, paths)
  Upload.find_by(model: self, path: paths)
end

#show_outdated_changes?Boolean

Returns:

  • (Boolean)

626
627
628
629
630
631
632
# File 'app/models/note.rb', line 626

def show_outdated_changes?
  return false unless for_merge_request?
  return false unless system?
  return false unless change_position&.line_range

  change_position.line_range["end"] || change_position.line_range["start"]
end

#skip_notification?Boolean

Returns:

  • (Boolean)

598
599
600
# File 'app/models/note.rb', line 598

def skip_notification?
  review.present? || !author.can_trigger_notifications?
end

#skip_project_check?Boolean

Returns:

  • (Boolean)

320
321
322
# File 'app/models/note.rb', line 320

def skip_project_check?
  !for_project_noteable?
end

#start_of_discussion?Boolean

Returns:

  • (Boolean)

473
474
475
# File 'app/models/note.rb', line 473

def start_of_discussion?
  discussion.first_note == self
end

#supports_suggestion?Boolean

Returns:

  • (Boolean)

272
273
274
# File 'app/models/note.rb', line 272

def supports_suggestion?
  false
end

#system_note_visible_for?(user) ⇒ Boolean

Returns:

  • (Boolean)

588
589
590
591
592
# File 'app/models/note.rb', line 588

def system_note_visible_for?(user)
  return true unless system?

  system_note_viewable_by?(user) && all_referenced_mentionables_allowed?(user)
end

#system_note_with_references?Boolean

rubocop: disable CodeReuse/ServiceClass

Returns:

  • (Boolean)

245
246
247
248
249
250
251
252
253
# File 'app/models/note.rb', line 245

def system_note_with_references?
  return unless system?

  if force_cross_reference_regex_check?
    matches_cross_reference_regex?
  else
    ::SystemNotes::IssuablesService.cross_reference?(note)
  end
end

#to_discussion(noteable = nil) ⇒ Object

Returns a discussion containing just this note. This method exists as an alternative to `#discussion` to use when the methods we intend to call on the Discussion object don't require it to have all of its notes, and just depend on the first note or the type of discussion. This saves us a DB query.


459
460
461
# File 'app/models/note.rb', line 459

def to_discussion(noteable = nil)
  Discussion.build([self], noteable)
end

#touch(*args, **kwargs) ⇒ Object


527
528
529
530
531
532
533
534
535
536
# File 'app/models/note.rb', line 527

def touch(*args, **kwargs)
  # We're not using an explicit transaction here because this would in all
  # cases result in all future queries going to the primary, even if no writes
  # are performed.
  #
  # We touch the noteable first so its SELECT query can run before our writes,
  # ensuring it runs on a secondary (if no prior write took place).
  touch_noteable
  super
end

#touch_noteableObject

By default Rails will issue an “SELECT *” for the relation, which is overkill for just updating the timestamps. To work around this we manually touch the data so we can SELECT only the columns we need.


541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'app/models/note.rb', line 541

def touch_noteable
  # Commits are not stored in the DB so we can't touch them.
  return if for_commit?

  assoc = association(:noteable)

  noteable_object =
    if assoc.loaded?
      noteable
    else
      # If the object is not loaded (e.g. when notes are loaded async) we
      # _only_ want the data we actually need.
      assoc.scope.select(:id, :updated_at).take
    end

  noteable_object&.touch

  # We return the noteable object so we can re-use it in EE for Elasticsearch.
  noteable_object
end

#user_mention_classObject


611
612
613
614
615
# File 'app/models/note.rb', line 611

def user_mention_class
  return if noteable.blank?

  noteable.user_mention_class
end

#user_mention_identifierObject


618
619
620
621
622
623
624
# File 'app/models/note.rb', line 618

def user_mention_identifier
  return if noteable.blank?

  noteable.user_mention_identifier.merge({
    note_id: id
  })
end

#user_mentionsObject


582
583
584
585
586
# File 'app/models/note.rb', line 582

def user_mentions
  return Note.none unless noteable.present?

  noteable.user_mentions.where(note: self)
end