Class: Issue

Direct Known Subclasses

WorkItem

Defined Under Namespace

Classes: Email, Metrics

Constant Summary collapse

DueDateStruct =
Struct.new(:title, :name).freeze
NoDueDate =
DueDateStruct.new('No Due Date', '0').freeze
AnyDueDate =
DueDateStruct.new('Any Due Date', 'any').freeze
Overdue =
DueDateStruct.new('Overdue', 'overdue').freeze
DueToday =
DueDateStruct.new('Due Today', 'today').freeze
DueTomorrow =
DueDateStruct.new('Due Tomorrow', 'tomorrow').freeze
DueThisWeek =
DueDateStruct.new('Due This Week', 'week').freeze
DueThisMonth =
DueDateStruct.new('Due This Month', 'month').freeze
DueNextMonthAndPreviousTwoWeeks =
DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
IssueTypeOutOfSyncError =
Class.new(StandardError)
SORTING_PREFERENCE_FIELD =
:issues_sort
MAX_BRANCH_TEMPLATE =
255
TYPES_FOR_LIST =

Types of issues that should be displayed on issue lists across the app for example, project issues list, group issues list, and issues dashboard.

This should be kept consistent with the enums used for the GraphQL issue list query in gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158

%w[issue incident test_case task objective key_result ticket].freeze
TYPES_FOR_BOARD_LIST =

Types of issues that should be displayed on issue board lists

%w[issue incident ticket].freeze
DEFAULT_ISSUE_TYPE =

This default came from the enum issue_type column. Defined as default in the DB

:issue

Constants included from PgFullTextSearchable

PgFullTextSearchable::LONG_WORDS_REGEX, PgFullTextSearchable::TEXT_SEARCH_DICTIONARY, PgFullTextSearchable::TSVECTOR_MAX_LENGTH, PgFullTextSearchable::VERY_LONG_WORDS_WITH_AT_REGEX, PgFullTextSearchable::XML_TAG_REGEX

Constants included from ThrottledTouch

ThrottledTouch::TOUCH_INTERVAL

Constants included from Gitlab::RelativePositioning

Gitlab::RelativePositioning::IDEAL_DISTANCE, Gitlab::RelativePositioning::IllegalRange, Gitlab::RelativePositioning::InvalidPosition, Gitlab::RelativePositioning::IssuePositioningDisabled, Gitlab::RelativePositioning::MAX_GAP, Gitlab::RelativePositioning::MAX_POSITION, Gitlab::RelativePositioning::MIN_GAP, Gitlab::RelativePositioning::MIN_POSITION, Gitlab::RelativePositioning::NoSpaceLeft, Gitlab::RelativePositioning::START_POSITION, Gitlab::RelativePositioning::STEPS

Constants included from Noteable

Noteable::MAX_NOTES_LIMIT

Constants included from Issuable

Issuable::DESCRIPTION_LENGTH_MAX, Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS, Issuable::SEARCHABLE_FIELDS, Issuable::STATE_ID_MAP, Issuable::TITLE_LENGTH_MAX

Constants included from Import::HasImportSource

Import::HasImportSource::IMPORT_SOURCES

Constants included from Taskable

Taskable::COMPLETED, Taskable::COMPLETE_PATTERN, Taskable::INCOMPLETE, Taskable::INCOMPLETE_PATTERN, Taskable::ITEM_PATTERN

Constants included from CacheMarkdownField

CacheMarkdownField::INVALIDATED_BY

Constants included from Redactable

Redactable::UNSUBSCRIBE_PATTERN

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_TERM

Constants included from AtomicInternalId

AtomicInternalId::MissingValueError

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from HasCheckConstraints

HasCheckConstraints::NOT_NULL_CHECK_PATTERN

Constants included from ResetOnColumnErrors

ResetOnColumnErrors::MAX_RESET_PERIOD

Instance Attribute Summary

Attributes included from Noteable

#system_note_timestamp

Attributes included from Transitionable

#transitioning

Attributes included from Importable

#importing, #user_contributions

Attributes included from CacheMarkdownField

#skip_markdown_cache_validation

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 PgFullTextSearchable

#update_search_data!

Methods included from IssueAvailableFeatures

#issue_type_supports?

Methods included from Presentable

#present

Methods included from ThrottledTouch

#touch

Methods included from TimeTrackable

#clear_memoized_total_time_spent, #human_time_change, #human_time_estimate, #human_total_time_spent, #reload, #reset, #set_time_estimate_default_value, #spend_time, #time_change, #time_estimate, #time_estimate=, #total_time_spent

Methods included from RelativePositioning

#exclude_self, #model_class, #move_after, #move_before, #move_between, #move_to_end, #move_to_start, mover, #relative_positioning_scoped_items, #reset_relative_position, #update_relative_siblings

Methods included from Gitlab::RelativePositioning

range

Methods included from FasterCacheKeys

#cache_key

Methods included from Spammable

#check_for_spam, #check_for_spam?, #clear_spam_flags!, #invalidate_if_spam, #needs_recaptcha!, #recaptcha_error!, #render_recaptcha?, #spam, #spam!, #spam_description, #spam_title, #spammable_attribute_changed?, #spammable_entity_type, #spammable_text, #submittable_as_spam?, #submittable_as_spam_by?, #unrecoverable_spam_error!

Methods included from Referable

#referable_inspect, #reference_link_text, #to_reference_base

Methods included from Noteable

#after_note_created, #after_note_destroyed, #base_class_name, #broadcast_notes_changed, #capped_notes_count, #commenters, #creatable_note_email_address, #discussion_ids_relation, #discussion_notes, #discussion_root_note_ids, #discussions, #discussions_can_be_resolved_by?, #discussions_resolvable?, #discussions_resolved?, #discussions_to_be_resolved, #grouped_diff_discussions, #has_any_diff_note_positions?, #human_class_name, #lockable?, #noteable_target_type_name, #preloads_discussion_diff_highlighting?, #resolvable_discussions, #supports_creating_notes_by_email?, #supports_discussions?, #supports_replying_to_individual_notes?, #supports_resolvable_notes?, #supports_suggestion?

Methods included from Issuable

#allows_scoped_labels?, #assignee?, #assignee_list, #assignee_or_author?, #assignee_username_list, #card_attributes, #draftless_title_changed, #first_contribution?, #hook_association_changes, #hook_reviewer_changes, #label_names, #labels_array, #labels_hook_attrs, #notes_for_participants, #notes_with_associations, #old_assignees, #old_escalation_status, #old_labels, #old_severity, #old_target_branch, #old_time_change, #old_total_time_spent, #open?, #overdue?, #read_ability_for, #reviewers_hook_attrs, #state, #state=, #subscribed_without_subscriptions?, #supports_health_status?, #to_ability_name, #to_hook_data, #updated_tasks, #user_notes_count

Methods included from Import::HasImportSource

#imported?

Methods included from ReportableChanges

#changes_applied, #clear_changes_information, #reload, #reportable_changes

Methods included from Exportable

#exportable_association?, #restricted_associations, #to_authorized_json

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from Editable

#edited?, #last_edited_by

Methods included from Transitionable

#disable_transitioning, #enable_transitioning, #transitioning?

Methods included from Taskable

#complete_task_list_item_count, get_tasks, get_updated_tasks, #task_completion_status, #task_list_items, #task_status, #task_status_short, #tasks?

Methods included from Awardable

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

Methods included from StripAttribute

#strip_attributes!

Methods included from Subscribable

#lazy_subscription, #set_subscription, #subscribe, #subscribed?, #subscribed_without_subscriptions?, #subscribers, #toggle_subscription, #unsubscribe

Methods included from Milestoneable

#milestone_available?, #supports_milestone?

Methods included from Mentionable

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

Methods included from Participable

#participant?, #participants, #visible_participants

Methods included from CacheMarkdownField

#attribute_invalidated?, #cached_html_for, #cached_html_up_to_date?, #can_cache_field?, #invalidated_markdown_cache?, #latest_cached_markdown_version, #mentionable_attributes_changed?, #mentioned_filtered_user_ids_for, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #store_mentions!, #store_mentions?, #store_mentions_after_commit?, #updated_cached_html_for

Methods included from Gitlab::SQL::Pattern

split_query_to_search_terms

Methods included from IidRoutes

#to_param

Methods included from AtomicInternalId

group_init, #internal_id_read_scope, #internal_id_scope_attrs, #internal_id_scope_usage, namespace_init, project_init, scope_attrs, scope_usage

Methods inherited from ApplicationRecord

===, cached_column_list, #create_or_load_association, current_transaction, declarative_enum, default_select_columns, delete_all_returning, #deleted_from_database?, id_in, id_not_in, iid_in, nullable_column?, 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 Organizations::Sharding

#sharding_organization

Methods included from ResetOnColumnErrors

#reset_on_union_error, #reset_on_unknown_attribute_error

Methods included from Gitlab::SensitiveSerializableHash

#serializable_hash

Class Method Details

.alternative_reference_prefix_with_postfixObject



437
438
439
# File 'app/models/issue.rb', line 437

def self.alternative_reference_prefix_with_postfix
  '[issue:'
end

.alternative_reference_prefix_without_postfixObject

Alternative prefix for situations where the standard prefix would be interpreted as a comment, most notably to begin commit messages with (e.g. “GL-123: My commit”)



433
434
435
# File 'app/models/issue.rb', line 433

def self.alternative_reference_prefix_without_postfix
  'GL-'
end

.column_order_id_ascObject



527
528
529
530
531
532
# File 'app/models/issue.rb', line 527

def self.column_order_id_asc
  Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
    attribute_name: 'id',
    order_expression: arel_table[:id].asc
  )
end

.column_order_relative_positionObject



518
519
520
521
522
523
524
525
# File 'app/models/issue.rb', line 518

def self.column_order_relative_position
  Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
    attribute_name: 'relative_position',
    column_expression: arel_table[:relative_position],
    order_expression: Issue.arel_table[:relative_position].asc.nulls_last,
    nullable: :nulls_last
  )
end

.full_search(query, matched_columns: nil, use_minimum_char_limit: true) ⇒ Object



369
370
371
372
373
374
375
376
# File 'app/models/issue.rb', line 369

def full_search(query, matched_columns: nil, use_minimum_char_limit: true)
  return super if query.match?(IssuableFinder::FULL_TEXT_SEARCH_TERM_REGEX)

  super.where(
    'issues.title NOT SIMILAR TO :pattern OR issues.description NOT SIMILAR TO :pattern',
    pattern: IssuableFinder::FULL_TEXT_SEARCH_TERM_PATTERN
  )
end

.in_namespaces_with_cte(namespaces) ⇒ Object



345
346
347
348
349
# File 'app/models/issue.rb', line 345

def in_namespaces_with_cte(namespaces)
  cte = Gitlab::SQL::CTE.new(:namespace_ids, namespaces.select(:id))

  where('issues.namespace_id IN (SELECT id FROM namespace_ids)').with(cte.to_arel)
end


469
470
471
# File 'app/models/issue.rb', line 469

def self.link_reference_pattern
  @link_reference_pattern ||= compose_link_reference_pattern(%r{issues(?:\/incident)?}, Gitlab::Regex.issue)
end

.order_by_relative_positionObject



514
515
516
# File 'app/models/issue.rb', line 514

def self.order_by_relative_position
  reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc]))
end

.order_upvotes_ascObject



364
365
366
# File 'app/models/issue.rb', line 364

def order_upvotes_asc
  reorder(upvotes_count: :asc)
end

.order_upvotes_descObject



359
360
361
# File 'app/models/issue.rb', line 359

def order_upvotes_desc
  reorder(upvotes_count: :desc)
end

.participant_includesObject



383
384
385
# File 'app/models/issue.rb', line 383

def self.participant_includes
  [:assignees] + super
end

.project_foreign_keyObject



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

def self.project_foreign_key
  'project_id'
end

.reference_patternObject

Pattern used to extract issue references from text This pattern supports cross-project references.



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'app/models/issue.rb', line 447

def self.reference_pattern
  prefix_with_postfix = alternative_reference_prefix_with_postfix
  if prefix_with_postfix.empty?
    @reference_pattern ||= %r{
    (?:
      (#{Project.reference_pattern})?#{Regexp.escape(reference_prefix)} |
      #{Regexp.escape(alternative_reference_prefix_without_postfix)}
    )#{Gitlab::Regex.issue}
  }x
  else
    %r{
  ((?:
    (#{Project.reference_pattern})?#{Regexp.escape(reference_prefix)} |
    #{alternative_reference_prefix_without_postfix}
  )#{Gitlab::Regex.issue}) |
  ((?:
    #{Regexp.escape(prefix_with_postfix)}(#{Project.reference_pattern}/)?
  )#{Gitlab::Regex.issue(reference_postfix)})
}x
  end
end

.reference_postfixObject



441
442
443
# File 'app/models/issue.rb', line 441

def self.reference_postfix
  ']'
end

.reference_prefixObject



426
427
428
# File 'app/models/issue.rb', line 426

def self.reference_prefix
  '#'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)


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

def self.reference_valid?(reference)
  reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end


378
379
380
# File 'app/models/issue.rb', line 378

def related_link_class
  IssueLink
end

.relative_positioning_parent_columnObject



422
423
424
# File 'app/models/issue.rb', line 422

def self.relative_positioning_parent_column
  :project_id
end

.relative_positioning_query_base(issue) ⇒ Object



418
419
420
# File 'app/models/issue.rb', line 418

def self.relative_positioning_query_base(issue)
  in_projects(issue.relative_positioning_parent_projects)
end

.simple_sortsObject



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

def self.simple_sorts
  super.merge(
    {
      'closest_future_date' => -> { order_closest_future_date },
      'closest_future_date_asc' => -> { order_closest_future_date },
      'due_date' => -> { order_due_date_asc.with_order_id_desc },
      'due_date_asc' => -> { order_due_date_asc.with_order_id_desc },
      'due_date_desc' => -> { order_due_date_desc.with_order_id_desc },
      'relative_position' => -> { order_by_relative_position },
      'relative_position_asc' => -> { order_by_relative_position }
    }
  )
end

.sort_by_attribute(method, excluded_labels: []) ⇒ Object



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'app/models/issue.rb', line 495

def self.sort_by_attribute(method, excluded_labels: [])
  case method.to_s
  when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
  when 'due_date', 'due_date_asc'                       then order_due_date_asc.with_order_id_desc
  when 'due_date_desc'                                  then order_due_date_desc.with_order_id_desc
  when 'start_date', 'start_date_asc'                   then order_start_date_asc.with_order_id_desc
  when 'start_date_desc'                                then order_start_date_desc.with_order_id_desc
  when 'relative_position', 'relative_position_asc'     then order_by_relative_position
  when 'severity_asc'                                   then order_severity_asc
  when 'severity_desc'                                  then order_severity_desc
  when 'escalation_status_asc'                          then order_escalation_status_asc
  when 'escalation_status_desc'                         then order_escalation_status_desc
  when 'closed_at', 'closed_at_asc'                     then order_closed_at_asc
  when 'closed_at_desc'                                 then order_closed_at_desc
  else
    super
  end
end

.supported_keyset_orderingsObject



560
561
562
563
564
565
566
567
568
569
# File 'app/models/issue.rb', line 560

def self.supported_keyset_orderings
  {
    id: [:asc, :desc],
    title: [:asc, :desc],
    created_at: [:asc, :desc],
    updated_at: [:asc, :desc],
    due_date: [:asc, :desc],
    relative_position: [:asc, :desc]
  }
end

.to_branch_name(id, title, project: nil) ⇒ Object



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'app/models/issue.rb', line 534

def self.to_branch_name(id, title, project: nil)
  params = {
    'id' => id.to_s.parameterize(preserve_case: true),
    'title' => title.to_s.parameterize
  }
  template = project&.issue_branch_template

  branch_name =
    if template.present?
      Gitlab::StringPlaceholderReplacer.replace_string_placeholders(template, /(#{params.keys.join('|')})/) do |arg|
        params[arg]
      end
    else
      params.values.select(&:present?).join('-')
    end

  if branch_name.length > 100
    truncated_string = branch_name[0, 100]
    # Delete everything dangling after the last hyphen so as not to risk
    # existence of unintended words in the branch name due to mid-word split.
    branch_name = truncated_string.sub(/-[^-]*\Z/, '')
  end

  branch_name
end

.with_accessible_sub_namespace_ids_cte(namespace_ids) ⇒ Object



351
352
353
354
355
356
# File 'app/models/issue.rb', line 351

def with_accessible_sub_namespace_ids_cte(namespace_ids)
  # Using materialized: true to ensure the CTE is computed once and reused, which significantly improves performance
  # for complex queries. See: https://gitlab.com/gitlab-org/gitlab/-/issues/548094
  accessible_sub_namespace_ids = Gitlab::SQL::CTE.new(:accessible_sub_namespace_ids, namespace_ids, materialized: true)
  with(accessible_sub_namespace_ids.to_arel)
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



926
927
928
929
930
# File 'app/models/issue.rb', line 926

def ==(other)
  return super unless id.present?

  other.is_a?(Issue) && other.id == id
end

#allow_possible_spam?(user) ⇒ Boolean

Always enforce spam check for support bot but allow for other users when issue is not publicly visible

Returns:

  • (Boolean)


670
671
672
673
674
675
# File 'app/models/issue.rb', line 670

def allow_possible_spam?(user)
  return true if Gitlab::CurrentSettings.allow_possible_spam
  return false if user.support_bot?

  !publicly_visible?
end

#as_json(options = {}) ⇒ Object



684
685
686
687
688
689
690
691
692
693
694
# File 'app/models/issue.rb', line 684

def as_json(options = {})
  super(options).tap do |json|
    if options.key?(:labels)
      json[:labels] = labels.as_json(
        project: project,
        only: [:id, :title, :description, :color, :priority],
        methods: [:text_color]
      )
    end
  end
end

#autoclose_by_merged_closing_merge_request?Boolean

Returns:

  • (Boolean)


872
873
874
875
876
# File 'app/models/issue.rb', line 872

def autoclose_by_merged_closing_merge_request?
  return false if group_level?

  project.autoclose_referenced_issues
end

#banzai_render_context(field) ⇒ Object



720
721
722
723
724
725
# File 'app/models/issue.rb', line 720

def banzai_render_context(field)
  additional_attributes = { label_url_method: :project_issues_url }
  additional_attributes[:group] = namespace if namespace.is_a?(Group)

  super.merge(additional_attributes)
end

#blocked_for_repositioning?Boolean

Returns:

  • (Boolean)


579
580
581
# File 'app/models/issue.rb', line 579

def blocked_for_repositioning?
  namespace.root_ancestor&.issue_repositioning_disabled?
end

#can_be_worked_on?Boolean

Returns:

  • (Boolean)


665
666
667
# File 'app/models/issue.rb', line 665

def can_be_worked_on?
  !self.closed? && !self.project.forked?
end

#can_move?(user, to_namespace = nil) ⇒ Boolean Also known as: can_clone?

Returns:

  • (Boolean)


623
624
625
626
627
628
629
# File 'app/models/issue.rb', line 623

def can_move?(user, to_namespace = nil)
  if to_namespace
    return false unless user.can?(:admin_issue, to_namespace)
  end

  !moved? && persisted? && user.can?(:admin_issue, self)
end

#check_repositioning_allowed!Object

Temporary disable moving null elements because of performance problems For more information check gitlab.com/gitlab-com/gl-infra/production/-/issues/4321



573
574
575
576
577
# File 'app/models/issue.rb', line 573

def check_repositioning_allowed!
  if blocked_for_repositioning?
    raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
  end
end

#clear_closure_reason_referencesObject



618
619
620
621
# File 'app/models/issue.rb', line 618

def clear_closure_reason_references
  self.moved_to_id = nil
  self.duplicated_to_id = nil
end

#design_collectionObject



727
728
729
# File 'app/models/issue.rb', line 727

def design_collection
  @design_collection ||= DesignManagement::DesignCollection.new(self)
end

#discussions_rendered_on_frontend?Boolean

Returns:

  • (Boolean)


700
701
702
# File 'app/models/issue.rb', line 700

def discussions_rendered_on_frontend?
  true
end

#duplicated?Boolean

Returns:

  • (Boolean)


614
615
616
# File 'app/models/issue.rb', line 614

def duplicated?
  !duplicated_to_id.nil?
end

#email_participants_emailsObject



761
762
763
# File 'app/models/issue.rb', line 761

def email_participants_emails
  issue_email_participants.pluck(:email)
end

#email_participants_emails_downcaseObject



765
766
767
# File 'app/models/issue.rb', line 765

def email_participants_emails_downcase
  issue_email_participants.pluck(IssueEmailParticipant.arel_table[:email].lower)
end

#ensure_work_item_descriptionObject



913
914
915
916
917
918
919
920
921
922
923
924
# File 'app/models/issue.rb', line 913

def ensure_work_item_description
  return if work_item_description.present?

  build_work_item_description(
    description: description,
    description_html: description_html,
    last_edited_at: last_edited_at,
    last_edited_by_id: last_edited_by_id,
    lock_version: lock_version,
    cached_markdown_version: cached_markdown_version
  )
end

#epic_work_item?Boolean

Returns:

  • (Boolean)


878
879
880
# File 'app/models/issue.rb', line 878

def epic_work_item?
  work_item_type&.epic?
end

#expire_etag_cacheObject



782
783
784
785
786
787
788
789
# File 'app/models/issue.rb', line 782

def expire_etag_cache
  # We don't expire the cache for issues that don't have a project, since they are created at the group level
  # and they are only displayed in the new work item view that uses GraphQL subscriptions for real-time updates
  return unless project

  key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
  Gitlab::EtagCaching::Store.new.touch(key)
end

#from_service_desk?Boolean

Returns:

  • (Boolean)


731
732
733
# File 'app/models/issue.rb', line 731

def from_service_desk?
  author.support_bot?
end

#gfm_reference(from = nil) ⇒ Object



825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
# File 'app/models/issue.rb', line 825

def gfm_reference(from = nil)
  # References can be ambiguous when two namespaces have the same path names. This is why we need to use absolute
  # paths when cross-referencing between projects and groups.
  #
  # Example setup:
  #  - `gitlab` group
  #  - `gitlab` project within the `gitlab` group
  #  - In a project issue, we reference an epic with `epic gitlab#123` for a system note
  #
  # When resolving this system note, we would look for a namespace within the `gitlab` project's parent.
  # This would be incorrect, as it would resolve to an ISSUE on the `gitlab` PROJECT, not the `gitlab` GROUP.
  #
  # This problem only occurs when there is a project with the same name as the group. Otherwise, our reference
  # code attempts to resolve it on the group.
  #
  # Since the reference parser has no information about where each reference originated, we need to fix this in
  # the reference itself by providing an absolute path.
  #
  # In the example above, the `from` argument is the Issue on the Project, and `self` is the item on the Group.
  # Every time we reference from a project to a group, we need to use an absolute path.
  # So we need to reference it as `epic /gitlab#123`.
  #
  # We could always use absolute paths to remove the ambiguity, but this would lead to longer references everywhere
  # that are harder to read.
  params = {}

  params[:full] = true if (from.is_a?(Project) || from.is_a?(Namespaces::ProjectNamespace)) && group_level?
  # When we reference a root group, we also need to use an absolute path because it could be that a namespace
  params[:absolute_path] = true unless namespace.has_parent?

  "#{work_item_type_with_default.name.underscore} #{to_reference(from, **params)}"
end

#group_epic_work_item?Boolean

Returns:

  • (Boolean)


882
883
884
# File 'app/models/issue.rb', line 882

def group_epic_work_item?
  epic_work_item? && group_level?
end

#group_level?Boolean

Returns:

  • (Boolean)


868
869
870
# File 'app/models/issue.rb', line 868

def group_level?
  project_id.blank?
end

#has_widget?(widget) ⇒ Boolean

Returns:

  • (Boolean)


862
863
864
865
866
# File 'app/models/issue.rb', line 862

def has_widget?(widget)
  widget_class = WorkItems::Widgets.const_get(widget.to_s.camelize, false)

  work_item_type.widget_classes(resource_parent).include?(widget_class)
end

#hidden?Boolean

Returns:

  • (Boolean)


778
779
780
# File 'app/models/issue.rb', line 778

def hidden?
  author&.banned?
end

#hook_attrsObject



820
821
822
# File 'app/models/issue.rb', line 820

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

#invalidate_project_counter_cachesObject

rubocop: disable CodeReuse/ServiceClass



705
706
707
708
709
# File 'app/models/issue.rb', line 705

def invalidate_project_counter_caches
  return unless project

  Projects::OpenIssuesCountService.new(project).delete_cache
end

#issue_assignee_user_idsObject



769
770
771
# File 'app/models/issue.rb', line 769

def issue_assignee_user_ids
  issue_assignees.pluck(:user_id)
end


735
736
737
738
739
740
741
742
743
# File 'app/models/issue.rb', line 735

def issue_link_type
  link_class = self.class.related_link_class
  return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)

  type = link_class.link_types.key(issue_link_type_value) || link_class::TYPE_RELATES_TO
  return type if issue_link_source_id == id

  link_class.inverse_link_type(type)
end

#issue_typeObject



816
817
818
# File 'app/models/issue.rb', line 816

def issue_type
  work_item_type_with_default.base_type
end

#issuing_parent_idObject



806
807
808
# File 'app/models/issue.rb', line 806

def issuing_parent_id
  project_id.presence || namespace_id
end

#linked_items_countObject



661
662
663
# File 'app/models/issue.rb', line 661

def linked_items_count
  related_issues(authorize: false).size
end

#merge_requests_count(user = nil) ⇒ Object

rubocop: enable CodeReuse/ServiceClass



712
713
714
# File 'app/models/issue.rb', line 712

def merge_requests_count(user = nil)
  ::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end

#moved?Boolean

Returns:

  • (Boolean)


610
611
612
# File 'app/models/issue.rb', line 610

def moved?
  !moved_to_id.nil?
end

#next_object_by_relative_position(ignoring: nil, order: :asc) ⇒ Object



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'app/models/issue.rb', line 387

def next_object_by_relative_position(ignoring: nil, order: :asc)
  array_mapping_scope = ->(id_expression) do
    relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression))

    if order == :asc
      relation.where(Issue.arel_table[:relative_position].gt(relative_position))
    else
      relation.where(Issue.arel_table[:relative_position].lt(relative_position))
    end
  end

  relation = Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
    scope: Issue.order(relative_position: order, id: order),
    array_scope: relative_positioning_parent_projects,
    array_mapping_scope: array_mapping_scope,
    finder_query: ->(_, id_expression) { Issue.where(Issue.arel_table[:id].eq(id_expression)) }
  ).execute

  relation = exclude_self(relation, excluded: ignoring) if ignoring.present?

  relation.take
end

#previous_updated_atObject



716
717
718
# File 'app/models/issue.rb', line 716

def previous_updated_at
  previous_changes['updated_at']&.first || updated_at
end

#real_time_notes_enabled?Boolean

Returns:

  • (Boolean)


696
697
698
# File 'app/models/issue.rb', line 696

def real_time_notes_enabled?
  true
end


640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
# File 'app/models/issue.rb', line 640

def related_issues(current_user = nil, authorize: true, preload: nil)
  return [] if new_record?

  related_issues =
    linked_issues_select
      .joins("INNER JOIN issue_links ON
         (issue_links.source_id = issues.id AND issue_links.target_id = #{id})
         OR
         (issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
      .preload(preload)
      .reorder('issue_link_id')

  related_issues = yield related_issues if block_given?
  return related_issues unless authorize

  cross_project_filter = ->(issues) { issues.where(project: project) }
  Ability.issues_readable_by_user(related_issues,
    current_user,
    filters: { read_cross_project: cross_project_filter })
end

#relative_positioning_parent_projectsObject



410
411
412
413
414
415
416
# File 'app/models/issue.rb', line 410

def relative_positioning_parent_projects
  if namespace.parent&.user_namespace?
    Project.id_in(namespace.project).select(:id)
  else
    namespace.root_ancestor&.all_projects&.select(:id)
  end
end

#relocation_targetObject



745
746
747
# File 'app/models/issue.rb', line 745

def relocation_target
  moved_to || duplicated_to
end

#require_legacy_views?Boolean

Legacy views/workflows only

  • Service Desk were not converted to the work items framework.

  • Incidents were not converted to the work items framework.

Returns:

  • (Boolean)


909
910
911
# File 'app/models/issue.rb', line 909

def require_legacy_views?
  from_service_desk? || work_item_type&.incident?
end

#resource_parentObject Also known as: issuing_parent



801
802
803
# File 'app/models/issue.rb', line 801

def resource_parent
  project || namespace
end

#show_as_work_item?Boolean

Some Issues types/conditions were not fully migrated to WorkItems UI/workflows yet. On the other hand some other Issue types/conditions are only available through WorkItems UI/workflows.

Overriden on EE (For OKRs and Epics)

Returns:

  • (Boolean)


898
899
900
901
902
903
904
# File 'app/models/issue.rb', line 898

def show_as_work_item?
  return false if require_legacy_views?
  return true if group_level?
  return true if work_item_type&.task?

  resource_parent.work_items_consolidated_list_enabled?
end

#skip_metrics?Boolean

Returns:

  • (Boolean)


858
859
860
# File 'app/models/issue.rb', line 858

def skip_metrics?
  importing?
end

#source_projectObject

To allow polymorphism with MergeRequest.



606
607
608
# File 'app/models/issue.rb', line 606

def source_project
  project
end

#suggested_branch_nameObject



590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'app/models/issue.rb', line 590

def suggested_branch_name
  return to_branch_name unless project.repository.branch_exists?(to_branch_name)

  start_counting_from = 2

  branch_name_generator = ->(counter) do
    suffix = counter > 5 ? SecureRandom.hex(8) : counter
    "#{to_branch_name}-#{suffix}"
  end

  Gitlab::Utils::Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
    project.repository.branch_exists?(suggested_branch_name)
  end
end

#supports_assignee?Boolean

Returns:

  • (Boolean)


749
750
751
# File 'app/models/issue.rb', line 749

def supports_assignee?
  work_item_type_with_default.supports_assignee?(resource_parent)
end

#supports_confidentiality?Boolean

Returns:

  • (Boolean)


791
792
793
# File 'app/models/issue.rb', line 791

def supports_confidentiality?
  true
end

#supports_move_and_clone?Boolean

Returns:

  • (Boolean)


757
758
759
# File 'app/models/issue.rb', line 757

def supports_move_and_clone?
  issue_type_supports?(:move_and_clone)
end

#supports_parent?Boolean

Overriden in EE

Returns:

  • (Boolean)


682
# File 'app/models/issue.rb', line 682

def supports_parent?; end

#supports_recaptcha?Boolean

Returns:

  • (Boolean)


677
678
679
# File 'app/models/issue.rb', line 677

def supports_recaptcha?
  true
end

#supports_time_tracking?Boolean

Returns:

  • (Boolean)


753
754
755
# File 'app/models/issue.rb', line 753

def supports_time_tracking?
  issue_type_supports?(:time_tracking)
end

#to_branch_nameObject



632
633
634
635
636
637
638
# File 'app/models/issue.rb', line 632

def to_branch_name
  if self.confidential?
    "#{iid}-confidential-issue"
  else
    self.class.to_branch_name(iid, title, project: project)
  end
end

#to_reference(from = nil, full: false, absolute_path: false) ⇒ Object

from argument can be a Namespace or Project.



584
585
586
587
588
# File 'app/models/issue.rb', line 584

def to_reference(from = nil, full: false, absolute_path: false)
  reference = "#{self.class.reference_prefix}#{iid}"

  "#{namespace.to_reference_base(from, full: full, absolute_path: absolute_path)}#{reference}"
end

#to_work_item_global_idObject

we want to have subscriptions working on work items only, legacy issues do not support graphql subscriptions, yet so we need sometimes GID of an issue instance to be represented as WorkItem GID. E.g. notes subscriptions.



797
798
799
# File 'app/models/issue.rb', line 797

def to_work_item_global_id
  ::Gitlab::GlobalId.as_global_id(id, model_name: WorkItem.name)
end

#update_upvotes_countObject



773
774
775
776
# File 'app/models/issue.rb', line 773

def update_upvotes_count
  self.lock!
  self.update_column(:upvotes_count, self.upvotes)
end

#use_work_item_url?Boolean

Returns:

  • (Boolean)


886
887
888
889
890
891
# File 'app/models/issue.rb', line 886

def use_work_item_url?
  return false if require_legacy_views?
  return true if work_item_type&.task?

  resource_parent.use_work_item_url?
end

#work_item_type_with_defaultObject

Persisted records will always have a work_item_type. This method is useful in places where we use a non persisted issue to perform feature checks



812
813
814
# File 'app/models/issue.rb', line 812

def work_item_type_with_default
  work_item_type || WorkItems::Type.default_by_type(DEFAULT_ISSUE_TYPE)
end