Class: MergeRequest

Defined Under Namespace

Classes: ApprovalRemovalSettings, CleanupSchedule, DiffCommitUser, Metrics, MetricsFinder

Constant Summary collapse

SORTING_PREFERENCE_FIELD =
:merge_requests_sort
KNOWN_MERGE_PARAMS =
[
  :auto_merge_strategy,
  :should_remove_source_branch,
  :force_remove_source_branch,
  :commit_message,
  :squash_commit_message,
  :sha,
  :skip_ci
].freeze
RebaseLockTimeout =
Class.new(StandardError)
DRAFT_REGEX =
/\A*#{Gitlab::Regex.merge_request_draft}+\s*/i
MAX_RECENT_DIFF_HEAD_SHAS =
100

Constants included from ReactiveCaching

ReactiveCaching::ExceededReactiveCacheLimit, ReactiveCaching::InvalidateReactiveCache, ReactiveCaching::WORK_TYPE

Constants included from ThrottledTouch

ThrottledTouch::TOUCH_INTERVAL

Constants included from Noteable

Noteable::MAX_NOTES_LIMIT

Constants included from Issuable

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

Constants included from Taskable

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

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 ResetOnUnionError

ResetOnUnionError::MAX_RESET_PERIOD

Instance Attribute Summary collapse

Attributes included from Noteable

#system_note_timestamp

Attributes included from Transitionable

#transitioning

Attributes included from Importable

#imported, #importing

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 Spammable

#allow_possible_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?, #supports_recaptcha?, #unrecoverable_spam_error!

Methods included from Approvable

#approved?, #approved_by?, #eligible_for_approval_by?, #eligible_for_unapproval_by?

Methods included from DeprecatedAssignee

#assignee, #assignee=, #assignee_id, #assignee_id=, #assignee_ids, #assignee_ids=, #assignees, #assignees=

Methods included from ThrottledTouch

#touch

Methods included from TimeTrackable

#human_time_change, #human_time_estimate, #human_total_time_spent, #set_time_estimate_default_value, #spend_time, #time_change, #time_estimate=, #total_time_spent

Methods included from Presentable

#present

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_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, #resolvable_discussions, #supports_creating_notes_by_email?, #supports_discussions?, #supports_replying_to_individual_notes?, #supports_resolvable_notes?

Methods included from Issuable

#allows_scoped_labels?, #assignee?, #assignee_list, #assignee_or_author?, #assignee_username_list, #can_assign_epic?, #can_move?, #card_attributes, #hook_association_changes, #hook_reviewer_changes, #label_names, #labels_array, #labels_hook_attrs, #notes_with_associations, #open?, #overdue?, #read_ability_for, #resource_parent, #state, #state=, #subscribed_without_subscriptions?, #supports_health_status?, #to_ability_name, #to_hook_data, #updated_tasks, #user_notes_count

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

get_tasks, get_updated_tasks, #task_completion_status, #task_list_items, #task_status, #task_status_short, #tasks, #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, #gfm_reference, #local_reference, #matches_cross_reference_regex?, #mentioned_users, #referenced_group_users, #referenced_groups, #referenced_mentionables, #referenced_project_users, #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, #local_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!, #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, 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

#allow_brokenObject

When this attribute is true some MR validation is ignored It allows us to close or modify broken merge requests



145
146
147
# File 'app/models/merge_request.rb', line 145

def allow_broken
  @allow_broken
end

#can_be_createdObject

Temporary fields to store compare vars when creating new merge request



153
154
155
# File 'app/models/merge_request.rb', line 153

def can_be_created
  @can_be_created
end

#compareObject

Temporary fields to store compare vars when creating new merge request



153
154
155
# File 'app/models/merge_request.rb', line 153

def compare
  @compare
end

#compare_commitsObject

Temporary fields to store compare vars when creating new merge request



153
154
155
# File 'app/models/merge_request.rb', line 153

def compare_commits
  @compare_commits
end

#diff_optionsObject

Temporary fields to store compare vars when creating new merge request



153
154
155
# File 'app/models/merge_request.rb', line 153

def diff_options
  @diff_options
end

#skip_ensure_merge_request_diffObject

Temporary flag to skip merge_request_diff creation on create. See gitlab.com/gitlab-org/gitlab/-/merge_requests/100390



149
150
151
# File 'app/models/merge_request.rb', line 149

def skip_ensure_merge_request_diff
  @skip_ensure_merge_request_diff
end

#skip_merge_status_triggerObject

Flag to skip triggering mergeRequestMergeStatusUpdated GraphQL subscription.



156
157
158
# File 'app/models/merge_request.rb', line 156

def skip_merge_status_trigger
  @skip_merge_status_trigger
end

#source_branch_shaObject



972
973
974
# File 'app/models/merge_request.rb', line 972

def source_branch_sha
  @source_branch_sha || source_branch_head.try(:sha)
end

#target_branch_shaObject



968
969
970
# File 'app/models/merge_request.rb', line 968

def target_branch_sha
  @target_branch_sha || target_branch_head.try(:sha)
end

Class Method Details

.available_state_namesObject

Keep states definition to be evaluated before the state_machine block to

avoid spec failures. If this gets evaluated after, the `merged` and `locked`
states (which are overridden) can be nil.


167
168
169
# File 'app/models/merge_request.rb', line 167

def self.available_state_names
  super + [:merged, :locked]
end

.draft?(title) ⇒ Boolean

Returns:

  • (Boolean)


644
645
646
# File 'app/models/merge_request.rb', line 644

def self.draft?(title)
  !!(title =~ DRAFT_REGEX)
end

.draft_title(title) ⇒ Object



652
653
654
# File 'app/models/merge_request.rb', line 652

def self.draft_title(title)
  draft?(title) ? title : "Draft: #{title}"
end

.draftless_title(title) ⇒ Object



648
649
650
# File 'app/models/merge_request.rb', line 648

def self.draftless_title(title)
  title.sub(DRAFT_REGEX, "")
end

.in_projects(relation) ⇒ Object

Returns all the merge requests from an ActiveRecord:Relation.

This method uses a UNION as it usually operates on the result of ProjectsFinder#execute. PostgreSQL in particular doesn’t always like queries using multiple sub-queries especially when combined with an OR statement. UNIONs on the other hand perform much better in these cases.

relation - An ActiveRecord::Relation that returns a list of Projects.

Returns an ActiveRecord::Relation.



613
614
615
616
617
618
619
620
# File 'app/models/merge_request.rb', line 613

def self.in_projects(relation)
  # unscoping unnecessary conditions that'll be applied
  # when executing `where("merge_requests.id IN (#{union.to_sql})")`
  source = unscoped.where(source_project_id: relation)
  target = unscoped.where(target_project_id: relation)

  from_union([source, target])
end


591
592
593
# File 'app/models/merge_request.rb', line 591

def self.link_reference_pattern
  @link_reference_pattern ||= compose_link_reference_pattern('merge_requests', Gitlab::Regex.merge_request)
end

.merge_request_ref?(ref) ⇒ Boolean

Returns:

  • (Boolean)


1589
1590
1591
# File 'app/models/merge_request.rb', line 1589

def self.merge_request_ref?(ref)
  ref.start_with?("refs/#{Repository::REF_MERGE_REQUEST}/")
end

.merge_train_ref?(ref) ⇒ Boolean

Returns:

  • (Boolean)


1593
1594
1595
# File 'app/models/merge_request.rb', line 1593

def self.merge_train_ref?(ref)
  %r{\Arefs/#{Repository::REF_MERGE_REQUEST}/\d+/train\z}o.match?(ref)
end

.participant_includesObject



662
663
664
# File 'app/models/merge_request.rb', line 662

def self.participant_includes
  [:assignees, :reviewers] + super
end

.project_foreign_keyObject



599
600
601
# File 'app/models/merge_request.rb', line 599

def self.project_foreign_key
  'target_project_id'
end

.recent_target_branches(limit: 100) ⇒ Object

Returns the top 100 target branches

The returned value is a Array containing branch names sort by updated_at of merge request:

['master', 'develop', 'production']

limit - The maximum number of target branch to return.



521
522
523
524
525
526
527
# File 'app/models/merge_request.rb', line 521

def self.recent_target_branches(limit: 100)
  group(:target_branch)
    .select(:target_branch)
    .reorder(arel_table[:updated_at].maximum.desc)
    .limit(limit)
    .pluck(:target_branch)
end

.reference_patternObject

Pattern used to extract ‘!123` merge request references from text

This pattern supports cross-project references.



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

def self.reference_pattern
  @reference_pattern ||= %r{
    (#{Project.reference_pattern})?
    #{Regexp.escape(reference_prefix)}#{Gitlab::Regex.merge_request}
  }x
end

.reference_prefixObject



509
510
511
# File 'app/models/merge_request.rb', line 509

def self.reference_prefix
  '!'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)


595
596
597
# File 'app/models/merge_request.rb', line 595

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

.reviewers_subqueryObject



540
541
542
543
544
# File 'app/models/merge_request.rb', line 540

def self.reviewers_subquery
  MergeRequestReviewer.arel_table
    .project('true')
    .where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
end

.set_latest_merge_request_diff_ids!Object

This is used after project import, to reset the IDs to the correct values. It is not intended to be called without having already scoped the relation.

Only set ‘regular` merge request diffs as latest so `merge_head` diff won’t be considered as ‘MergeRequest#merge_request_diff`.



628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'app/models/merge_request.rb', line 628

def self.set_latest_merge_request_diff_ids!
  update = "
    latest_merge_request_diff_id = (
      SELECT MAX(id)
      FROM merge_request_diffs
      WHERE merge_requests.id = merge_request_diffs.merge_request_id
      AND merge_request_diffs.diff_type = #{MergeRequestDiff.diff_types[:regular]}
    )".squish

  self.each_batch do |batch|
    batch.update_all(update)
  end
end

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



529
530
531
532
533
534
535
536
537
538
# File 'app/models/merge_request.rb', line 529

def self.sort_by_attribute(method, excluded_labels: [])
  case method.to_s
  when 'merged_at', 'merged_at_asc' then order_merged_at_asc
  when 'closed_at', 'closed_at_asc' then order_closed_at_asc
  when 'merged_at_desc' then order_merged_at_desc
  when 'closed_at_desc' then order_closed_at_desc
  else
    super
  end
end

.total_time_to_mergeObject



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

def self.total_time_to_merge
  join_metrics
    .where(
      # Replicating the scope MergeRequest::Metrics.with_valid_time_to_merge
      MergeRequest::Metrics.arel_table[:merged_at].gt(
        MergeRequest::Metrics.arel_table[:created_at]
      )
    )
    .pick(MergeRequest::Metrics.time_to_merge_expression)
end

.wip_titleObject



659
660
661
# File 'app/models/merge_request.rb', line 659

def self.draft_title(title)
  draft?(title) ? title : "Draft: #{title}"
end

.wipless_titleObject



658
659
660
# File 'app/models/merge_request.rb', line 658

def self.draftless_title(title)
  title.sub(DRAFT_REGEX, "")
end

.work_in_progress?Boolean

Returns:

  • (Boolean)


657
658
659
# File 'app/models/merge_request.rb', line 657

def self.draft?(title)
  !!(title =~ DRAFT_REGEX)
end

Instance Method Details

#actual_head_pipelineObject

Use this method whenever you need to make sure the head_pipeline is synced with the branch head commit, for example checking if a merge request can be merged. For more information check: gitlab.com/gitlab-org/gitlab-foss/issues/40004



559
560
561
# File 'app/models/merge_request.rb', line 559

def actual_head_pipeline
  head_pipeline&.matches_sha_or_source_sha?(diff_head_sha) ? head_pipeline : nil
end

#actual_head_pipeline_active?Boolean

Returns:

  • (Boolean)


573
574
575
# File 'app/models/merge_request.rb', line 573

def actual_head_pipeline_active?
  !!actual_head_pipeline&.active?
end

#actual_head_pipeline_success?Boolean

Returns:

  • (Boolean)


577
578
579
# File 'app/models/merge_request.rb', line 577

def actual_head_pipeline_success?
  !!actual_head_pipeline&.success?
end

#all_commit_shasObject

Note that this could also return SHA from now dangling commits



1829
1830
1831
1832
1833
1834
1835
# File 'app/models/merge_request.rb', line 1829

def all_commit_shas
  @all_commit_shas ||= begin
    return commit_shas unless persisted?

    all_commits.pluck(:sha).uniq
  end
end

#all_commitsObject



1821
1822
1823
1824
1825
# File 'app/models/merge_request.rb', line 1821

def all_commits
  MergeRequestDiffCommit
    .where(merge_request_diff: merge_request_diffs.recent)
    .limit(10_000)
end

#all_pipelinesObject



1639
1640
1641
1642
1643
# File 'app/models/merge_request.rb', line 1639

def all_pipelines
  strong_memoize(:all_pipelines) do
    Ci::PipelinesForMergeRequestFinder.new(self, nil).all
  end
end

#allow_collaborationObject Also known as: allow_collaboration?



1991
1992
1993
# File 'app/models/merge_request.rb', line 1991

def allow_collaboration
  collaborative_push_possible? && allow_maintainer_to_push
end

#allows_multiple_reviewers?Boolean

Returns:

  • (Boolean)


2033
2034
2035
# File 'app/models/merge_request.rb', line 2033

def allows_multiple_reviewers?
  false
end

#allows_reviewers?Boolean

Returns:

  • (Boolean)


2029
2030
2031
# File 'app/models/merge_request.rb', line 2029

def allows_reviewers?
  true
end

#async_cleanup_refs(only: :all) ⇒ Object



1585
1586
1587
# File 'app/models/merge_request.rb', line 1585

def async_cleanup_refs(only: :all)
  project.repository.async_delete_refs(*refs_to_cleanup(only: only))
end

#auto_merge_strategyObject



1298
1299
1300
1301
1302
# File 'app/models/merge_request.rb', line 1298

def auto_merge_strategy
  return unless auto_merge_enabled?

  merge_params['auto_merge_strategy'] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
end

#auto_merge_strategy=(strategy) ⇒ Object



1304
1305
1306
# File 'app/models/merge_request.rb', line 1304

def auto_merge_strategy=(strategy)
  merge_params['auto_merge_strategy'] = strategy
end

#banzai_render_context(field) ⇒ Object



2021
2022
2023
# File 'app/models/merge_request.rb', line 2021

def banzai_render_context(field)
  super.merge(label_url_method: :project_merge_requests_url)
end

#base_pipelineObject



1959
1960
1961
1962
1963
# File 'app/models/merge_request.rb', line 1959

def base_pipeline
  @base_pipeline ||= project.ci_pipelines
    .order(id: :desc)
    .find_by(sha: diff_base_sha, ref: target_branch)
end

#branch_merge_base_commitObject



959
960
961
962
963
964
965
966
# File 'app/models/merge_request.rb', line 959

def branch_merge_base_commit
  start_sha = target_branch_sha
  head_sha  = source_branch_sha

  if start_sha && head_sha
    target_project.merge_base_commit(start_sha, head_sha)
  end
end

#branch_merge_base_shaObject



996
997
998
# File 'app/models/merge_request.rb', line 996

def branch_merge_base_sha
  branch_merge_base_commit.try(:sha)
end

#branch_missing?Boolean

Returns:

  • (Boolean)


1508
1509
1510
# File 'app/models/merge_request.rb', line 1508

def branch_missing?
  !source_branch_exists? || !target_branch_exists?
end

#broken?Boolean

Returns:

  • (Boolean)


1512
1513
1514
# File 'app/models/merge_request.rb', line 1512

def broken?
  has_no_commits? || branch_missing? || cannot_be_merged?
end

#cache_merge_request_closes_issues!(current_user = self.author) ⇒ Object

If the merge request closes any issues, save this information in the ‘MergeRequestsClosingIssues` model. This is a performance optimization. Calculating this information for a number of merge requests requires running `ReferenceExtractor` on each of them separately. This optimization does not apply to issues from external sources.



1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
# File 'app/models/merge_request.rb', line 1364

def cache_merge_request_closes_issues!(current_user = self.author)
  return if closed? || merged?

  transaction do
    self.merge_requests_closing_issues.delete_all

    closes_issues(current_user).each do |issue|
      next if issue.is_a?(ExternalIssue)

      self.merge_requests_closing_issues.create!(issue: issue)
    end
  end
end

#calculate_reactive_cache(identifier, current_user_id = nil, report_type = nil, *args) ⇒ Object

Raises:

  • (NameError)


1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
# File 'app/models/merge_request.rb', line 1801

def calculate_reactive_cache(identifier, current_user_id = nil, report_type = nil, *args)
  service_class = identifier.constantize

  # TODO: the type check should change to something that includes exposed artifacts service
  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
  raise NameError, service_class unless service_class < Ci::CompareReportsBaseService

  current_user = User.find_by(id: current_user_id)
  service_class.new(project, current_user, id: id, report_type: report_type).execute(comparison_base_pipeline(service_class), actual_head_pipeline)
end

#can_allow_collaboration?(user) ⇒ Boolean

Returns:

  • (Boolean)


2004
2005
2006
2007
# File 'app/models/merge_request.rb', line 2004

def can_allow_collaboration?(user)
  collaborative_push_possible? &&
    Ability.allowed?(user, :push_code, source_project)
end

#can_be_cherry_picked?Boolean

Returns:

  • (Boolean)


1884
1885
1886
# File 'app/models/merge_request.rb', line 1884

def can_be_cherry_picked?
  merge_commit.present?
end

#can_be_closed?Boolean

Returns:

  • (Boolean)


1095
1096
1097
# File 'app/models/merge_request.rb', line 1095

def can_be_closed?
  opened?
end

#can_be_merged_by?(user, skip_collaboration_check: false) ⇒ Boolean

Returns:

  • (Boolean)


1516
1517
1518
1519
# File 'app/models/merge_request.rb', line 1516

def can_be_merged_by?(user, skip_collaboration_check: false)
  access = ::Gitlab::UserAccess.new(user, container: project, skip_collaboration_check: skip_collaboration_check)
  access.can_update_branch?(target_branch)
end

#can_be_merged_via_command_line_by?(user) ⇒ Boolean

Returns:

  • (Boolean)


1521
1522
1523
1524
# File 'app/models/merge_request.rb', line 1521

def can_be_merged_via_command_line_by?(user)
  access = ::Gitlab::UserAccess.new(user, container: project)
  access.can_push_to_branch?(target_branch)
end

#can_be_reverted?(current_user) ⇒ Boolean

Returns:

  • (Boolean)


1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
# File 'app/models/merge_request.rb', line 1858

def can_be_reverted?(current_user)
  return false unless merge_commit
  return false unless merged_at

  # It is not guaranteed that Note#created_at will be strictly later than
  # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
  # comparison, as will a HA environment if clocks are not *precisely*
  # synchronized. Add a minute's leeway to compensate for both possibilities
  cutoff = merged_at - 1.minute

  notes_association = notes_with_associations.where('created_at >= ?', cutoff)

  !merge_commit.has_been_reverted?(current_user, notes_association)
end

#can_cancel_auto_merge?(current_user) ⇒ Boolean

Returns:

  • (Boolean)


1278
1279
1280
# File 'app/models/merge_request.rb', line 1278

def can_cancel_auto_merge?(current_user)
  can_be_merged_by?(current_user) || self.author == current_user
end

#can_remove_source_branch?(current_user) ⇒ Boolean

Returns:

  • (Boolean)


1282
1283
1284
1285
1286
1287
1288
# File 'app/models/merge_request.rb', line 1282

def can_remove_source_branch?(current_user)
  source_project &&
    !ProtectedBranch.protected?(source_project, source_branch) &&
    !source_project.root_ref?(source_branch) &&
    Ability.allowed?(current_user, :push_code, source_project) &&
    diff_head_sha == source_branch_head.try(:sha)
end

#can_suggest_reviewers?Boolean

Returns:

  • (Boolean)


2090
2091
2092
# File 'app/models/merge_request.rb', line 2090

def can_suggest_reviewers?
  false # overridden in EE
end

#check_for_spam?Boolean

Returns:

  • (Boolean)


2110
2111
2112
# File 'app/models/merge_request.rb', line 2110

def check_for_spam?(*)
  spammable_attribute_changed? && project.public?
end

#check_mergeability(async: false, sync_retry_lease: false) ⇒ Object



1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
# File 'app/models/merge_request.rb', line 1182

def check_mergeability(async: false, sync_retry_lease: false)
  return unless recheck_merge_status?

  check_service = MergeRequests::MergeabilityCheckService.new(self)

  if async
    check_service.async_execute
  else
    check_service.execute(retry_lease: sync_retry_lease)
  end
end

#cleanup_refs(only: :all) ⇒ Object



1581
1582
1583
# File 'app/models/merge_request.rb', line 1581

def cleanup_refs(only: :all)
  project.repository.delete_refs(*refs_to_cleanup(only: only))
end

#clear_memoized_shasObject



1161
1162
1163
1164
1165
1166
# File 'app/models/merge_request.rb', line 1161

def clear_memoized_shas
  @target_branch_sha = @source_branch_sha = nil

  clear_memoization(:source_branch_head)
  clear_memoization(:target_branch_head)
end

#closed_eventObject



1209
1210
1211
# File 'app/models/merge_request.rb', line 1209

def closed_event
  @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :closed).last
end

#closed_or_merged_without_fork?Boolean

Returns:

  • (Boolean)


1080
1081
1082
# File 'app/models/merge_request.rb', line 1080

def closed_or_merged_without_fork?
  (closed? || merged?) && source_project_missing?
end

#closes_issues(current_user = self.author) ⇒ Object

Return the set of issues that will be closed if this merge request is accepted.



1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
# File 'app/models/merge_request.rb', line 1391

def closes_issues(current_user = self.author)
  if target_branch == project.default_branch
    messages = [title, description]
    messages.concat(commits.map(&:safe_message)) if merge_request_diff.persisted?

    Gitlab::ClosingIssueExtractor.new(project, current_user)
      .closed_by_message(messages.join("\n"))
  else
    []
  end
end

#collaborative_push_possible?Boolean

Returns:

  • (Boolean)


1997
1998
1999
2000
2001
2002
# File 'app/models/merge_request.rb', line 1997

def collaborative_push_possible?
  source_project.present? && for_fork? &&
    target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
    source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
    !ProtectedBranch.protected?(source_project, source_branch)
end

#commit_notesObject



1335
1336
1337
1338
1339
1340
1341
1342
1343
# File 'app/models/merge_request.rb', line 1335

def commit_notes
  # Fetch comments only from last 100 commits
  commit_ids = commit_shas(limit: 100)

  Note
    .user
    .where(project_id: [source_project_id, target_project_id])
    .for_commit_id(commit_ids)
end

#commit_shas(limit: nil) ⇒ Object



727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/merge_request.rb', line 727

def commit_shas(limit: nil)
  return merge_request_diff.commit_shas(limit: limit) if merge_request_diff.persisted?

  shas =
    if compare_commits
      compare_commits.to_a.reverse.map(&:sha)
    else
      Array(diff_head_sha)
    end

  limit ? shas.take(limit) : shas
end

#commits(limit: nil, load_from_gitaly: false, page: nil) ⇒ Object



700
701
702
703
704
705
706
707
708
709
710
711
# File 'app/models/merge_request.rb', line 700

def commits(limit: nil, load_from_gitaly: false, page: nil)
  return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page) if merge_request_diff.persisted?

  commits_arr = if compare_commits
                  reversed_commits = compare_commits.reverse
                  limit ? reversed_commits.take(limit) : reversed_commits
                else
                  []
                end

  CommitCollection.new(source_project, commits_arr, source_branch)
end

#commits_countObject



717
718
719
720
721
722
723
724
725
# File 'app/models/merge_request.rb', line 717

def commits_count
  if merge_request_diff.persisted?
    merge_request_diff.commits_count
  elsif compare_commits
    compare_commits.size
  else
    0
  end
end

#committers(with_merge_commits: false) ⇒ Object



666
667
668
# File 'app/models/merge_request.rb', line 666

def committers(with_merge_commits: false)
  @committers ||= commits.committers(with_merge_commits: with_merge_commits)
end

#compare_accessibility_reportsObject



1695
1696
1697
1698
1699
1700
1701
# File 'app/models/merge_request.rb', line 1695

def compare_accessibility_reports
  unless has_accessibility_reports?
    return { status: :error, status_reason: _('This merge request does not have accessibility reports') }
  end

  compare_reports(Ci::CompareAccessibilityReportsService)
end

#compare_codequality_reportsObject



1735
1736
1737
1738
1739
1740
1741
# File 'app/models/merge_request.rb', line 1735

def compare_codequality_reports
  unless has_codequality_reports?
    return { status: :error, status_reason: _('This merge request does not have codequality reports') }
  end

  compare_reports(Ci::CompareCodequalityReportsService)
end

#compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {}) ⇒ Object

TODO: consider renaming this as with exposed artifacts we generate reports, not always compare issue: gitlab.com/gitlab-org/gitlab/issues/34224



1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
# File 'app/models/merge_request.rb', line 1770

def compare_reports(service_class, current_user = nil, report_type = nil, additional_params = {})
  with_reactive_cache(service_class.name, current_user&.id, report_type) do |data|
    unless service_class.new(project, current_user, id: id, report_type: report_type, additional_params: additional_params)
      .latest?(comparison_base_pipeline(service_class), actual_head_pipeline, data)
      raise InvalidateReactiveCache
    end

    data
  end || { status: :parsing }
end

#compare_sast_reports(current_user) ⇒ Object



1789
1790
1791
1792
1793
# File 'app/models/merge_request.rb', line 1789

def compare_sast_reports(current_user)
  return missing_report_error("SAST") unless has_sast_reports?

  compare_reports(::Ci::CompareSecurityReportsService, current_user, 'sast')
end

#compare_secret_detection_reports(current_user) ⇒ Object



1795
1796
1797
1798
1799
# File 'app/models/merge_request.rb', line 1795

def compare_secret_detection_reports(current_user)
  return missing_report_error("secret detection") unless has_secret_detection_reports?

  compare_reports(::Ci::CompareSecurityReportsService, current_user, 'secret_detection')
end

#compare_test_reportsObject



1675
1676
1677
1678
1679
1680
1681
# File 'app/models/merge_request.rb', line 1675

def compare_test_reports
  unless has_test_reports?
    return { status: :error, status_reason: 'This merge request does not have test reports' }
  end

  compare_reports(Ci::CompareTestReportsService)
end

#comparison_base_pipeline(service_class) ⇒ Object



1955
1956
1957
# File 'app/models/merge_request.rb', line 1955

def comparison_base_pipeline(service_class)
  (use_merge_base_pipeline_for_comparison?(service_class) && merge_base_pipeline) || base_pipeline
end

#conflicting_mr_message(conflicting_mr) ⇒ Object



1034
1035
1036
1037
1038
1039
# File 'app/models/merge_request.rb', line 1034

def conflicting_mr_message(conflicting_mr)
  format(
    _("Another open merge request already exists for this source branch: %{conflicting_mr_reference}"),
    conflicting_mr_reference: conflicting_mr.to_reference
  )
end

#context_commits(limit: nil) ⇒ Object



688
689
690
# File 'app/models/merge_request.rb', line 688

def context_commits(limit: nil)
  @context_commits ||= merge_request_context_commits.order_by_committed_date_desc.limit(limit).map(&:to_commit)
end

#context_commits_countObject



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

def context_commits_count
  context_commits.count
end

#context_commits_diffObject



2070
2071
2072
2073
2074
# File 'app/models/merge_request.rb', line 2070

def context_commits_diff
  strong_memoize(:context_commits_diff) do
    ContextCommitsDiff.new(self)
  end
end

#create_merge_request_diffObject



1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
# File 'app/models/merge_request.rb', line 1117

def create_merge_request_diff
  # Callers such as MergeRequests::BuildService may not call eager_fetch_ref!. Just
  # in case they haven't, we fetch the ref.
  fetch_ref! unless skip_fetch_ref

  # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377
  Gitlab::GitalyClient.allow_n_plus_1_calls do
    merge_request_diffs.create!
    reload_merge_request_diff
  end
end

#current_patch_id_shaObject



2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
# File 'app/models/merge_request.rb', line 2118

def current_patch_id_sha
  return merge_request_diff.patch_id_sha if merge_request_diff.patch_id_sha.present?

  base_sha = diff_refs&.base_sha
  head_sha = diff_refs&.head_sha

  return unless base_sha && head_sha
  return if base_sha == head_sha

  project.repository.get_patch_id(base_sha, head_sha)
end

#default_merge_commit_message(include_description: false, user: nil) ⇒ Object



1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
# File 'app/models/merge_request.rb', line 1456

def default_merge_commit_message(include_description: false, user: nil)
  if self.target_project.merge_commit_template.present? && !include_description
    return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).merge_commit_message
  end

  closes_issues_references = visible_closing_issues_for.map do |issue|
    issue.to_reference(target_project)
  end

  message = [
    "Merge branch '#{source_branch}' into '#{target_branch}'",
    title
  ]

  if !include_description && closes_issues_references.present?
    message << "Closes #{closes_issues_references.to_sentence}"
  end

  message << description if include_description && description.present?
  message << "See merge request #{to_reference(full: true)}"

  message.join("\n\n")
end

#default_squash_commit_message(user: nil) ⇒ Object



1480
1481
1482
1483
1484
1485
1486
# File 'app/models/merge_request.rb', line 1480

def default_squash_commit_message(user: nil)
  if self.target_project.squash_commit_template.present?
    return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).squash_commit_message
  end

  title
end

#diff_base_commitObject



877
878
879
880
881
882
883
# File 'app/models/merge_request.rb', line 877

def diff_base_commit
  if merge_request_diff.persisted?
    merge_request_diff.base_commit
  else
    branch_merge_base_commit
  end
end

#diff_base_shaObject



909
910
911
912
913
914
915
# File 'app/models/merge_request.rb', line 909

def diff_base_sha
  if merge_request_diff.persisted?
    merge_request_diff.base_commit_sha
  else
    branch_merge_base_commit.try(:sha)
  end
end

#diff_head_commitObject



893
894
895
896
897
898
899
# File 'app/models/merge_request.rb', line 893

def diff_head_commit
  if merge_request_diff.persisted?
    merge_request_diff.head_commit
  else
    source_branch_head
  end
end

#diff_head_shaObject



917
918
919
920
921
922
923
# File 'app/models/merge_request.rb', line 917

def diff_head_sha
  if merge_request_diff.persisted?
    merge_request_diff.head_commit_sha
  else
    source_branch_head.try(:sha)
  end
end

#diff_refsObject



976
977
978
979
980
981
982
# File 'app/models/merge_request.rb', line 976

def diff_refs
  if importing? || persisted?
    merge_request_diff.diff_refs
  else
    repository_diff_refs
  end
end

#diff_sizeObject



857
858
859
860
861
# File 'app/models/merge_request.rb', line 857

def diff_size
  # Calling `merge_request_diff.diffs.real_size` will also perform
  # highlighting, which we don't need here.
  merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size
end

#diff_start_commitObject



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

def diff_start_commit
  if merge_request_diff.persisted?
    merge_request_diff.start_commit
  else
    target_branch_head
  end
end

#diff_start_shaObject



901
902
903
904
905
906
907
# File 'app/models/merge_request.rb', line 901

def diff_start_sha
  if merge_request_diff.persisted?
    merge_request_diff.start_commit_sha
  else
    target_branch_head.try(:sha)
  end
end

#diff_statsObject



849
850
851
852
853
854
855
# File 'app/models/merge_request.rb', line 849

def diff_stats
  return unless diff_refs

  strong_memoize(:diff_stats) do
    project.repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha)
  end
end

#diffable_merge_ref?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)


1195
1196
1197
# File 'app/models/merge_request.rb', line 1195

def diffable_merge_ref?
  open? && merge_head_diff.present? && can_be_merged?
end

#diffs(diff_options = {}) ⇒ Object



802
803
804
805
806
807
808
809
810
811
# File 'app/models/merge_request.rb', line 802

def diffs(diff_options = {})
  if compare
    # When saving MR diffs, `expanded` is implicitly added (because we need
    # to save the entire contents to the DB), so add that here for
    # consistency.
    compare.diffs(diff_options.merge(expanded: true))
  else
    merge_request_diff.diffs(diff_options)
  end
end

#diffs_batch_cache_with_max_age?Boolean

Returns:

  • (Boolean)


2098
2099
2100
# File 'app/models/merge_request.rb', line 2098

def diffs_batch_cache_with_max_age?
  Feature.enabled?(:diffs_batch_cache_with_max_age, project)
end

#discussions_diffsObject



838
839
840
841
842
843
844
845
846
847
# File 'app/models/merge_request.rb', line 838

def discussions_diffs
  strong_memoize(:discussions_diffs) do
    note_diff_files = NoteDiffFile
      .joins(:diff_note)
      .merge(notes.or(commit_notes))
      .includes(diff_note: :project)

    Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
  end
end

#discussions_rendered_on_frontend?Boolean

Returns:

  • (Boolean)


1971
1972
1973
# File 'app/models/merge_request.rb', line 1971

def discussions_rendered_on_frontend?
  true
end

#diverged_commits_countObject



1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
# File 'app/models/merge_request.rb', line 1612

def diverged_commits_count
  cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

  if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
    cache = {
      source_sha: source_branch_sha,
      target_sha: target_branch_sha,
      diverged_commits_count: compute_diverged_commits_count
    }
    Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
  end

  cache[:diverged_commits_count]
end

#diverged_from_target_branch?Boolean

Returns:

  • (Boolean)


1635
1636
1637
# File 'app/models/merge_request.rb', line 1635

def diverged_from_target_branch?
  diverged_commits_count > 0
end

#draft?Boolean Also known as: work_in_progress?

Returns:

  • (Boolean)


1213
1214
1215
# File 'app/models/merge_request.rb', line 1213

def draft?
  self.class.draft?(title)
end

#draft_titleObject Also known as: wip_title



1223
1224
1225
# File 'app/models/merge_request.rb', line 1223

def draft_title
  self.class.draft_title(self.title)
end

#draftless_titleObject Also known as: wipless_title



1218
1219
1220
# File 'app/models/merge_request.rb', line 1218

def draftless_title
  self.class.draftless_title(self.title)
end

#draftless_title_changed(old_title) ⇒ Object Also known as: wipless_title_changed

Verifies if title has changed not taking into account Draft prefix for merge requests.



672
673
674
# File 'app/models/merge_request.rb', line 672

def draftless_title_changed(old_title)
  self.class.draftless_title(old_title) != self.draftless_title
end

#eager_fetch_ref!Object



1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
# File 'app/models/merge_request.rb', line 1103

def eager_fetch_ref!
  return unless valid?

  # has_internal_id normally attempts to allocate the iid in the
  # before_create hook, but we need the iid to be available before
  # that to fetch the ref into the target project.
  track_target_project_iid!
  ensure_target_project_iid!

  fetch_ref!
  # Prevent the after_create hook from fetching the source branch again.
  @skip_fetch_ref = true
end

#enabled_reportsObject



2057
2058
2059
2060
2061
2062
# File 'app/models/merge_request.rb', line 2057

def enabled_reports
  {
    sast: report_type_enabled?(:sast),
    secret_detection: report_type_enabled?(:secret_detection)
  }
end

#ensure_merge_request_diffObject



1099
1100
1101
# File 'app/models/merge_request.rb', line 1099

def ensure_merge_request_diff
  merge_request_diff.persisted? || create_merge_request_diff
end

#ensure_metrics!Object



2025
2026
2027
# File 'app/models/merge_request.rb', line 2025

def ensure_metrics!
  MergeRequest::Metrics.record!(self)
end

#environments_in_head_pipeline(deployment_status: nil) ⇒ Object



1534
1535
1536
# File 'app/models/merge_request.rb', line 1534

def environments_in_head_pipeline(deployment_status: nil)
  actual_head_pipeline&.environments_in_self_and_project_descendants(deployment_status: deployment_status) || Environment.none
end

#execute_merge_checks(params: {}) ⇒ Object



2084
2085
2086
2087
2088
# File 'app/models/merge_request.rb', line 2084

def execute_merge_checks(params: {})
  # rubocop: disable CodeReuse/ServiceClass
  MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute
  # rubocop: enable CodeReuse/ServiceClass
end

#existing_mrs_targeting_same_branchObject



1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/merge_request.rb', line 1000

def existing_mrs_targeting_same_branch
  similar_mrs = target_project
      .merge_requests
      .where(source_branch: source_branch, target_branch: target_branch)
      .where(source_project: source_project)
      .opened

  similar_mrs = similar_mrs.id_not_in(id) if persisted?

  similar_mrs
end

#fetch_ref!Object



1538
1539
1540
1541
# File 'app/models/merge_request.rb', line 1538

def fetch_ref!
  target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
  expire_ancestor_cache
end

#ff_merge_possible?Boolean

Returns:

  • (Boolean)


1270
1271
1272
# File 'app/models/merge_request.rb', line 1270

def ff_merge_possible?
  project.repository.ancestor?(target_branch_sha, diff_head_sha)
end

#find_actual_head_pipelineObject



2009
2010
2011
# File 'app/models/merge_request.rb', line 2009

def find_actual_head_pipeline
  all_pipelines.for_sha_or_source_sha(diff_head_sha).first
end

#find_assignee(user) ⇒ Object



2041
2042
2043
# File 'app/models/merge_request.rb', line 2041

def find_assignee(user)
  merge_request_assignees.find_by(user_id: user.id)
end

#find_codequality_mr_diff_reportsObject

TODO: this method and compare_test_reports use the same result type, which is handled by the controller’s #reports_response. we should minimize mistakes by isolating the common parts. issue: gitlab.com/gitlab-org/gitlab/issues/34224



1723
1724
1725
1726
1727
1728
1729
# File 'app/models/merge_request.rb', line 1723

def find_codequality_mr_diff_reports
  unless has_codequality_mr_diff_report?
    return { status: :error, status_reason: 'This merge request does not have codequality mr diff reports' }
  end

  compare_reports(Ci::GenerateCodequalityMrDiffReportService)
end

#find_coverage_reportsObject

TODO: this method and compare_test_reports use the same result type, which is handled by the controller’s #reports_response. we should minimize mistakes by isolating the common parts. issue: gitlab.com/gitlab-org/gitlab/issues/34224



1707
1708
1709
1710
1711
1712
1713
# File 'app/models/merge_request.rb', line 1707

def find_coverage_reports
  unless has_coverage_reports?
    return { status: :error, status_reason: 'This merge request does not have coverage reports' }
  end

  compare_reports(Ci::GenerateCoverageReportsService)
end

#find_exposed_artifactsObject

TODO: this method and compare_test_reports use the same result type, which is handled by the controller’s #reports_response. we should minimize mistakes by isolating the common parts. issue: gitlab.com/gitlab-org/gitlab/issues/34224



1759
1760
1761
1762
1763
1764
1765
# File 'app/models/merge_request.rb', line 1759

def find_exposed_artifacts
  unless has_exposed_artifacts?
    return { status: :error, status_reason: 'This merge request does not have exposed artifacts' }
  end

  compare_reports(Ci::GenerateExposedArtifactsReportService)
end

#find_reviewer(user) ⇒ Object



2049
2050
2051
# File 'app/models/merge_request.rb', line 2049

def find_reviewer(user)
  merge_request_reviewers.find_by(user_id: user.id)
end

#find_terraform_reportsObject



1743
1744
1745
1746
1747
1748
1749
# File 'app/models/merge_request.rb', line 1743

def find_terraform_reports
  unless has_terraform_reports?
    return { status: :error, status_reason: 'This merge request does not have terraform reports' }
  end

  compare_reports(Ci::GenerateTerraformReportsService)
end

#first_commitObject



794
795
796
# File 'app/models/merge_request.rb', line 794

def first_commit
  compare_commits.present? ? compare_commits.first : merge_request_diff.first_commit
end

#first_contribution?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)


1981
1982
1983
1984
1985
# File 'app/models/merge_request.rb', line 1981

def first_contribution?
  return metrics&.first_contribution if merged? & metrics.present?

  !project.merge_requests.merged.exists?(author_id: author_id)
end

#first_multiline_commitObject

Returns the oldest multi-line commit



1489
1490
1491
1492
1493
# File 'app/models/merge_request.rb', line 1489

def first_multiline_commit
  strong_memoize(:first_multiline_commit) do
    recent_commits.without_merge_commits.reverse_each.find(&:description?)
  end
end

#for_fork?Boolean

Returns:

  • (Boolean)


1351
1352
1353
# File 'app/models/merge_request.rb', line 1351

def for_fork?
  target_project != source_project
end

#for_same_project?Boolean

Returns:

  • (Boolean)


1355
1356
1357
# File 'app/models/merge_request.rb', line 1355

def for_same_project?
  target_project == source_project
end

#force_remove_source_branch?Boolean

Returns:

  • (Boolean)


1294
1295
1296
# File 'app/models/merge_request.rb', line 1294

def force_remove_source_branch?
  Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
end

#has_accessibility_reports?Boolean

Returns:

  • (Boolean)


1683
1684
1685
# File 'app/models/merge_request.rb', line 1683

def has_accessibility_reports?
  actual_head_pipeline.present? && actual_head_pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:accessibility))
end

#has_ci?Boolean

Returns:

  • (Boolean)


1502
1503
1504
1505
1506
# File 'app/models/merge_request.rb', line 1502

def has_ci?
  return false if has_no_commits?

  !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_integration)
end

#has_codequality_mr_diff_report?Boolean

Returns:

  • (Boolean)


1715
1716
1717
# File 'app/models/merge_request.rb', line 1715

def has_codequality_mr_diff_report?
  actual_head_pipeline&.has_codequality_mr_diff_report?
end

#has_codequality_reports?Boolean

Returns:

  • (Boolean)


1731
1732
1733
# File 'app/models/merge_request.rb', line 1731

def has_codequality_reports?
  actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end

#has_commits?Boolean

Returns:

  • (Boolean)


1936
1937
1938
# File 'app/models/merge_request.rb', line 1936

def has_commits?
  merge_request_diff.persisted? && commits_count.to_i > 0
end

#has_complete_diff_refs?Boolean

Returns:

  • (Boolean)


1888
1889
1890
# File 'app/models/merge_request.rb', line 1888

def has_complete_diff_refs?
  diff_refs && diff_refs.complete?
end

#has_coverage_reports?Boolean

Returns:

  • (Boolean)


1687
1688
1689
# File 'app/models/merge_request.rb', line 1687

def has_coverage_reports?
  actual_head_pipeline&.has_coverage_reports?
end

#has_exposed_artifacts?Boolean

Returns:

  • (Boolean)


1751
1752
1753
# File 'app/models/merge_request.rb', line 1751

def has_exposed_artifacts?
  actual_head_pipeline&.has_exposed_artifacts?
end

#has_no_commits?Boolean

Returns:

  • (Boolean)


1940
1941
1942
# File 'app/models/merge_request.rb', line 1940

def has_no_commits?
  !has_commits?
end

#has_sast_reports?Boolean

Returns:

  • (Boolean)


1781
1782
1783
# File 'app/models/merge_request.rb', line 1781

def has_sast_reports?
  !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:sast))
end

#has_secret_detection_reports?Boolean

Returns:

  • (Boolean)


1785
1786
1787
# File 'app/models/merge_request.rb', line 1785

def has_secret_detection_reports?
  !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:secret_detection))
end

#has_terraform_reports?Boolean

Returns:

  • (Boolean)


1691
1692
1693
# File 'app/models/merge_request.rb', line 1691

def has_terraform_reports?
  actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform))
end

#has_test_reports?Boolean

Returns:

  • (Boolean)


1652
1653
1654
# File 'app/models/merge_request.rb', line 1652

def has_test_reports?
  actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test))
end

#head_pipeline_active?Boolean

Returns:

  • (Boolean)


569
570
571
# File 'app/models/merge_request.rb', line 569

def head_pipeline_active?
  !!head_pipeline&.active?
end

#hidden?Boolean

Returns:

  • (Boolean)


2094
2095
2096
# File 'app/models/merge_request.rb', line 2094

def hidden?
  Feature.enabled?(:hide_merge_requests_from_banned_users) && author&.banned?
end

#hook_attrsObject



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

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

#in_locked_stateObject



1597
1598
1599
1600
1601
1602
# File 'app/models/merge_request.rb', line 1597

def in_locked_state
  lock_mr
  yield
ensure
  unlock_mr if locked?
end

#includes_ci_config?Boolean

Returns:

  • (Boolean)


2064
2065
2066
2067
2068
# File 'app/models/merge_request.rb', line 2064

def includes_ci_config?
  return false unless diff_stats

  diff_stats.map(&:path).include?(project.ci_config_path_or_default)
end

#issues_mentioned_but_not_closing(current_user) ⇒ Object



1403
1404
1405
1406
1407
1408
1409
1410
# File 'app/models/merge_request.rb', line 1403

def issues_mentioned_but_not_closing(current_user)
  return [] unless target_branch == project.default_branch

  ext = Gitlab::ReferenceExtractor.new(project, current_user)
  ext.analyze("#{title}\n#{description}")

  ext.issues - visible_closing_issues_for(current_user)
end

#keep_around_commitObject

rubocop: enable CodeReuse/ServiceClass



1932
1933
1934
# File 'app/models/merge_request.rb', line 1932

def keep_around_commit
  project.repository.keep_around(self.merge_commit_sha)
end

#merge_async(user_id, params) ⇒ Object

Calls ‘MergeWorker` to proceed with the merge process and updates `merge_jid` with the MergeWorker#jid. This helps tracking enqueued and ongoing merge jobs.



753
754
755
756
757
758
759
760
# File 'app/models/merge_request.rb', line 753

def merge_async(user_id, params)
  jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h)
  update_column(:merge_jid, jid)

  # merge_ongoing? depends on merge_jid
  # expire etag cache since the attribute is changed without triggering callbacks
  expire_etag_cache
end

#merge_base_pipelineObject



1965
1966
1967
1968
1969
# File 'app/models/merge_request.rb', line 1965

def merge_base_pipeline
  @merge_base_pipeline ||= project.ci_pipelines
    .order(id: :desc)
    .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
end

#merge_blocked_by_other_mrs?Boolean

Returns:

  • (Boolean)


2080
2081
2082
# File 'app/models/merge_request.rb', line 2080

def merge_blocked_by_other_mrs?
  false # Overridden in EE
end

#merge_commitObject



1837
1838
1839
# File 'app/models/merge_request.rb', line 1837

def merge_commit
  @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end

#merge_eventObject



1205
1206
1207
# File 'app/models/merge_request.rb', line 1205

def merge_event
  @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :merged).last
end

#merge_ongoing?Boolean

Returns:

  • (Boolean)


1072
1073
1074
1075
1076
1077
1078
# File 'app/models/merge_request.rb', line 1072

def merge_ongoing?
  # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
  # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
  return true if locked?

  !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
end

#merge_participantsObject



784
785
786
787
788
789
790
791
792
# File 'app/models/merge_request.rb', line 784

def merge_participants
  participants = [author]

  if auto_merge_enabled? && !participants.include?(merge_user)
    participants << merge_user
  end

  participants.select { |participant| Ability.allowed?(participant, :read_merge_request, self) }
end

#merge_pipelineObject



563
564
565
566
567
# File 'app/models/merge_request.rb', line 563

def merge_pipeline
  if sha = merged_commit_sha
    target_project.latest_pipeline(target_branch, sha)
  end
end

#merge_ref_headObject

Returns the current merge-ref HEAD commit.



1545
1546
1547
1548
1549
# File 'app/models/merge_request.rb', line 1545

def merge_ref_head
  return project.repository.commit(merge_ref_sha) if merge_ref_sha

  project.repository.commit(merge_ref_path)
end

#merge_ref_pathObject



1555
1556
1557
# File 'app/models/merge_request.rb', line 1555

def merge_ref_path
  "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge"
end

#merge_request_assignees_with(user_ids) ⇒ Object



2045
2046
2047
# File 'app/models/merge_request.rb', line 2045

def merge_request_assignees_with(user_ids)
  merge_request_assignees.where(user_id: user_ids)
end

#merge_request_diffObject

This is the same as latest_merge_request_diff unless:

  1. There are arguments - in which case we might be trying to force-reload.

  2. This association is already loaded.

  3. The latest diff does not exist.

  4. It doesn’t have any merge_request_diffs - it returns an empty MergeRequestDiff

The second one in particular is important - MergeRequestDiff#merge_request is the inverse of MergeRequest#merge_request_diff, which means it may not be the latest diff, because we could have loaded any diff from this particular MR. If we haven’t already loaded a diff, then it’s fine to load the latest.



84
85
86
87
88
# File 'app/models/merge_request.rb', line 84

def merge_request_diff
  fallback = latest_merge_request_diff unless association(:merge_request_diff).loaded?

  fallback || super || MergeRequestDiff.new(merge_request_id: id)
end

#merge_request_diff_for(diff_refs_or_sha) ⇒ Object



1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
# File 'app/models/merge_request.rb', line 1133

def merge_request_diff_for(diff_refs_or_sha)
  matcher =
    if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
      {
        'start_commit_sha' => diff_refs_or_sha.start_sha,
        'head_commit_sha' => diff_refs_or_sha.head_sha,
        'base_commit_sha' => diff_refs_or_sha.base_sha
      }
    else
      { 'head_commit_sha' => diff_refs_or_sha }
    end

  viewable_diffs.find do |diff|
    diff.attributes.slice(*matcher.keys) == matcher
  end
end

#merge_request_reviewers_with(user_ids) ⇒ Object



2053
2054
2055
# File 'app/models/merge_request.rb', line 2053

def merge_request_reviewers_with(user_ids)
  merge_request_reviewers.where(user_id: user_ids)
end

#mergeability_checksObject



1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
# File 'app/models/merge_request.rb', line 1246

def mergeability_checks
  # We want to have the cheapest checks first in the list, that way we can
  #   fail fast before running the more expensive ones.
  #
  [
    ::MergeRequests::Mergeability::CheckOpenStatusService,
    ::MergeRequests::Mergeability::CheckDraftStatusService,
    ::MergeRequests::Mergeability::CheckBrokenStatusService,
    ::MergeRequests::Mergeability::CheckDiscussionsStatusService,
    ::MergeRequests::Mergeability::CheckCiStatusService
  ]
end

#mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false) ⇒ Boolean

Returns:

  • (Boolean)


1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
# File 'app/models/merge_request.rb', line 1234

def mergeable?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false, skip_rebase_check: false)
  return false unless mergeable_state?(
    skip_ci_check: skip_ci_check,
    skip_discussions_check: skip_discussions_check,
    skip_approved_check: skip_approved_check
  )

  check_mergeability(sync_retry_lease: check_mergeability_retry_lease)

  can_be_merged? && (!should_be_rebased? || skip_rebase_check)
end

#mergeable_ci_state?Boolean

Returns:

  • (Boolean)


1526
1527
1528
1529
1530
1531
1532
# File 'app/models/merge_request.rb', line 1526

def mergeable_ci_state?
  return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
  return false unless actual_head_pipeline
  return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped?

  actual_head_pipeline.success?
end

#mergeable_discussions_state?Boolean

Returns:

  • (Boolean)


1345
1346
1347
1348
1349
# File 'app/models/merge_request.rb', line 1345

def mergeable_discussions_state?
  return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)

  unresolved_notes.none?(&:to_be_resolved?)
end

#mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false) ⇒ Boolean

Returns:

  • (Boolean)


1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
# File 'app/models/merge_request.rb', line 1259

def mergeable_state?(skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false)
  additional_checks = execute_merge_checks(
    params: {
      skip_ci_check: skip_ci_check,
      skip_discussions_check: skip_discussions_check,
      skip_approved_check: skip_approved_check
    }
  )
  additional_checks.success?
end

#merged_atObject



1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
# File 'app/models/merge_request.rb', line 1873

def merged_at
  strong_memoize(:merged_at) do
    next unless merged?

    metrics&.merged_at ||
      merge_event&.created_at ||
      resource_state_events.find_by(state: :merged)&.created_at ||
      notes.system.reorder(nil).find_by(note: 'merged')&.created_at
  end
end

#merged_commit_shaObject



1845
1846
1847
1848
1849
1850
# File 'app/models/merge_request.rb', line 1845

def merged_commit_sha
  return unless merged?

  sha = super || merge_commit_sha || squash_commit_sha || diff_head_sha
  sha.presence
end

#missing_required_squash?Boolean

Returns:

  • (Boolean)


2114
2115
2116
# File 'app/models/merge_request.rb', line 2114

def missing_required_squash?
  !squash && target_project.squash_always?
end

#modified_paths(past_merge_request_diff: nil, fallback_on_overflow: false) ⇒ Object



863
864
865
866
867
868
869
870
871
# File 'app/models/merge_request.rb', line 863

def modified_paths(past_merge_request_diff: nil, fallback_on_overflow: false)
  if past_merge_request_diff
    past_merge_request_diff.modified_paths(fallback_on_overflow: fallback_on_overflow)
  elsif compare
    diff_stats&.paths || compare.modified_paths
  else
    merge_request_diff.modified_paths(fallback_on_overflow: fallback_on_overflow)
  end
end

#new_pathsObject



873
874
875
# File 'app/models/merge_request.rb', line 873

def new_paths
  diffs.diff_files.map(&:new_path)
end

#non_latest_diffsObject



813
814
815
# File 'app/models/merge_request.rb', line 813

def non_latest_diffs
  merge_request_diffs.where.not(id: merge_request_diff.id)
end

#note_positions_for_paths(paths, user = nil) ⇒ Object



817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
# File 'app/models/merge_request.rb', line 817

def note_positions_for_paths(paths, user = nil)
  positions = notes.new_diff_notes.joins(:note_diff_file)
    .where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths)
    .positions

  collection = Gitlab::Diff::PositionCollection.new(positions, diff_head_sha)

  return collection unless user

  positions = draft_notes
    .authored_by(user)
    .positions
    .select { |pos| paths.include?(pos.file_path) }

  collection.concat(positions)
end

#notify_conflict?Boolean

Returns:

  • (Boolean)


1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
# File 'app/models/merge_request.rb', line 1312

def notify_conflict?
  (opened? || locked?) &&
    has_commits? &&
    !branch_missing? &&
    !project.repository.can_be_merged?(diff_head_sha, target_branch)
rescue Gitlab::Git::CommandError
  # Checking mergeability can trigger exception, e.g. non-utf8
  # We ignore this type of errors.
  false
end

#permits_force_push?Boolean

Returns:

  • (Boolean)


550
551
552
553
554
# File 'app/models/merge_request.rb', line 550

def permits_force_push?
  return true unless ProtectedBranch.protected?(source_project, source_branch)

  ProtectedBranch.allow_force_push?(source_project, source_branch)
end

#pipeline_coverage_deltaObject



1944
1945
1946
1947
1948
# File 'app/models/merge_request.rb', line 1944

def pipeline_coverage_delta
  if base_pipeline&.coverage && head_pipeline&.coverage
    head_pipeline.coverage - base_pipeline.coverage
  end
end

#predefined_variablesObject



1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
# File 'app/models/merge_request.rb', line 1656

def predefined_variables
  Gitlab::Ci::Variables::Collection.new.tap do |variables|
    variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
    variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
    variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', value: ref_path.to_s)
    variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', value: project.id.to_s)
    variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path)
    variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
    variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
    variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED', value: ProtectedBranch.protected?(target_project, target_branch).to_s)
    variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
    variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present?
    variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
    variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
    variables.append(key: 'CI_MERGE_REQUEST_SQUASH_ON_MERGE', value: squash_on_merge?.to_s)
    variables.concat(source_project_variables)
  end
end

#preloads_discussion_diff_highlighting?Boolean

Returns:

  • (Boolean)


834
835
836
# File 'app/models/merge_request.rb', line 834

def preloads_discussion_diff_highlighting?
  true
end

#prepareObject



2106
2107
2108
# File 'app/models/merge_request.rb', line 2106

def prepare
  NewMergeRequestWorker.perform_async(id, author_id)
end

#prepared?Boolean

Returns:

  • (Boolean)


2102
2103
2104
# File 'app/models/merge_request.rb', line 2102

def prepared?
  prepared_at.present?
end

#public_merge_statusObject

Returns current merge_status except it returns ‘cannot_be_merged_rechecking` as `checking` to avoid exposing unnecessary internal state



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

def public_merge_status
  cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status
end

#raw_diffs(*args) ⇒ Object



798
799
800
# File 'app/models/merge_request.rb', line 798

def raw_diffs(*args)
  compare.present? ? compare.raw_diffs(*args) : merge_request_diff.raw_diffs(*args)
end

#real_time_notes_enabled?Boolean

Returns:

  • (Boolean)


2013
2014
2015
# File 'app/models/merge_request.rb', line 2013

def real_time_notes_enabled?
  true
end

#rebase_async(user_id, skip_ci: false) ⇒ Object

Set off a rebase asynchronously, atomically updating the ‘rebase_jid` of the MR so that the status of the operation can be tracked.



764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
# File 'app/models/merge_request.rb', line 764

def rebase_async(user_id, skip_ci: false)
  with_rebase_lock do
    raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress?

    # Although there is a race between setting rebase_jid here and clearing it
    # in the RebaseWorker, it can't do any harm since we check both that the
    # attribute is set *and* that the sidekiq job is still running. So a JID
    # for a completed RebaseWorker is equivalent to a nil JID.
    jid = Sidekiq::Worker.skipping_transaction_check do
      RebaseWorker.with_status.perform_async(id, user_id, skip_ci)
    end

    update_column(:rebase_jid, jid)
  end

  # rebase_in_progress? depends on rebase_jid
  # expire etag cache since the attribute is changed without triggering callbacks
  expire_etag_cache
end

#rebase_in_progress?Boolean

Returns:

  • (Boolean)


546
547
548
# File 'app/models/merge_request.rb', line 546

def rebase_in_progress?
  rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
end

#recent_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: false, page: nil) ⇒ Object



713
714
715
# File 'app/models/merge_request.rb', line 713

def recent_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: false, page: nil)
  commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page)
end

#recent_context_commitsObject



692
693
694
# File 'app/models/merge_request.rb', line 692

def recent_context_commits
  context_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE)
end

#recent_diff_head_shas(limit = MAX_RECENT_DIFF_HEAD_SHAS) ⇒ Object



1814
1815
1816
1817
1818
1819
# File 'app/models/merge_request.rb', line 1814

def recent_diff_head_shas(limit = MAX_RECENT_DIFF_HEAD_SHAS)
  # see MergeRequestDiff.recent
  return merge_request_diffs.to_a.sort_by(&:id).reverse.first(limit).pluck(:head_commit_sha) if merge_request_diffs.loaded?

  merge_request_diffs.recent(limit).pluck(:head_commit_sha)
end

#recent_visible_deploymentsObject



2017
2018
2019
# File 'app/models/merge_request.rb', line 2017

def recent_visible_deployments
  deployments.visible.includes(:environment).order(id: :desc).limit(10)
end

#recheck_merge_status?Boolean

Returns boolean indicating the merge_status should be rechecked in order to switch to either can_be_merged or cannot_be_merged.

Returns:

  • (Boolean)


1201
1202
1203
# File 'app/models/merge_request.rb', line 1201

def recheck_merge_status?
  self.class.state_machines[:merge_status].check_state?(merge_status)
end

#ref_pathObject



1551
1552
1553
# File 'app/models/merge_request.rb', line 1551

def ref_path
  "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end

#refs_to_cleanup(only: :all) ⇒ Object



1573
1574
1575
1576
1577
1578
1579
# File 'app/models/merge_request.rb', line 1573

def refs_to_cleanup(only: :all)
  target_refs = []
  target_refs << ref_path       if %i[all head].include?(only)
  target_refs << merge_ref_path if %i[all merge].include?(only)
  target_refs << train_ref_path if %i[all train].include?(only)
  target_refs
end


1323
1324
1325
1326
1327
1328
1329
1330
1331
# File 'app/models/merge_request.rb', line 1323

def related_notes
  # We're using a UNION ALL here since this results in better performance
  # compared to using OR statements. We're using UNION ALL since the queries
  # used won't produce any duplicates (e.g. a note for a commit can't also be
  # a note for an MR).
  Note
    .from_union([notes, commit_notes], remove_duplicates: false)
    .includes(:noteable)
end

#reload_diff(current_user = nil) ⇒ Object

rubocop: disable CodeReuse/ServiceClass



1176
1177
1178
1179
1180
# File 'app/models/merge_request.rb', line 1176

def reload_diff(current_user = nil)
  return unless open?

  MergeRequests::ReloadDiffsService.new(self, current_user).execute
end

#reload_diff_if_branch_changedObject



1168
1169
1170
1171
1172
1173
# File 'app/models/merge_request.rb', line 1168

def reload_diff_if_branch_changed
  if (saved_change_to_source_branch? || saved_change_to_target_branch?) &&
      (source_branch_head && target_branch_head)
    reload_diff
  end
end

#remove_source_branch?Boolean

Returns:

  • (Boolean)


1308
1309
1310
# File 'app/models/merge_request.rb', line 1308

def remove_source_branch?
  should_remove_source_branch? || force_remove_source_branch?
end

#reopenable?Boolean

Returns:

  • (Boolean)


1091
1092
1093
# File 'app/models/merge_request.rb', line 1091

def reopenable?
  closed? && !source_project_missing? && source_branch_exists?
end

#repository_diff_refsObject

Instead trying to fetch the persisted diff_refs, this method goes straight to the repository to get the most recent data possible.



988
989
990
991
992
993
994
# File 'app/models/merge_request.rb', line 988

def repository_diff_refs
  Gitlab::Diff::DiffRefs.new(
    base_sha: branch_merge_base_sha,
    start_sha: target_branch_sha,
    head_sha: source_branch_sha
  )
end

#schedule_cleanup_refs(only: :all) ⇒ Object



1563
1564
1565
1566
1567
1568
1569
1570
1571
# File 'app/models/merge_request.rb', line 1563

def schedule_cleanup_refs(only: :all)
  if Feature.enabled?(:merge_request_delete_gitaly_refs_in_batches, target_project)
    async_cleanup_refs(only: only)
  elsif Feature.enabled?(:merge_request_cleanup_ref_worker_async, target_project)
    MergeRequests::CleanupRefWorker.perform_async(id, only.to_s)
  else
    cleanup_refs(only: only)
  end
end

#short_merge_commit_shaObject



1841
1842
1843
# File 'app/models/merge_request.rb', line 1841

def short_merge_commit_sha
  Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
end

#short_merged_commit_shaObject



1852
1853
1854
1855
1856
# File 'app/models/merge_request.rb', line 1852

def short_merged_commit_sha
  if sha = merged_commit_sha
    Commit.truncate_sha(sha)
  end
end

#should_be_rebased?Boolean

Returns:

  • (Boolean)


1274
1275
1276
# File 'app/models/merge_request.rb', line 1274

def should_be_rebased?
  project.ff_merge_must_be_possible? && !ff_merge_possible?
end

#should_remove_source_branch?Boolean

Returns:

  • (Boolean)


1290
1291
1292
# File 'app/models/merge_request.rb', line 1290

def should_remove_source_branch?
  Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
end

#skipped_mergeable_checks(options = {}) ⇒ Object



1228
1229
1230
1231
1232
# File 'app/models/merge_request.rb', line 1228

def skipped_mergeable_checks(options = {})
  {
    skip_ci_check: options.fetch(:auto_merge_requested, false)
  }
end

#source_branch_exists?Boolean

Returns:

  • (Boolean)


1444
1445
1446
1447
1448
# File 'app/models/merge_request.rb', line 1444

def source_branch_exists?
  return false unless self.source_project

  self.source_project.repository.branch_exists?(self.source_branch)
end

#source_branch_headObject



945
946
947
948
949
950
951
# File 'app/models/merge_request.rb', line 945

def source_branch_head
  strong_memoize(:source_branch_head) do
    if source_project && source_branch_ref
      source_project.repository.commit(source_branch_ref)
    end
  end
end

#source_branch_refObject



931
932
933
934
935
936
# File 'app/models/merge_request.rb', line 931

def source_branch_ref
  return @source_branch_sha if @source_branch_sha
  return unless source_branch

  Gitlab::Git::BRANCH_REF_PREFIX + source_branch
end

#source_project_missing?Boolean

Returns:

  • (Boolean)


1084
1085
1086
1087
1088
1089
# File 'app/models/merge_request.rb', line 1084

def source_project_missing?
  return false unless for_fork?
  return true unless source_project

  !source_project.in_fork_network_of?(target_project)
end

#source_project_namespaceObject



1428
1429
1430
1431
1432
1433
1434
# File 'app/models/merge_request.rb', line 1428

def source_project_namespace
  if source_project && source_project.namespace
    source_project.namespace.full_path
  else
    "(removed)"
  end
end

#source_project_pathObject



1420
1421
1422
1423
1424
1425
1426
# File 'app/models/merge_request.rb', line 1420

def source_project_path
  if source_project
    source_project.full_path
  else
    "(removed)"
  end
end

#squash_on_merge?Boolean

Returns:

  • (Boolean)


1495
1496
1497
1498
1499
1500
# File 'app/models/merge_request.rb', line 1495

def squash_on_merge?
  return true if target_project.squash_always?
  return false if target_project.squash_never?

  squash?
end

#suggested_reviewer_usersObject

method overridden in EE



70
71
72
# File 'app/models/merge_request.rb', line 70

def suggested_reviewer_users
  User.none
end

#supports_assignee?Boolean

Returns:

  • (Boolean)


2037
2038
2039
# File 'app/models/merge_request.rb', line 2037

def supports_assignee?
  true
end

#supports_lock_on_merge?Boolean

Returns:

  • (Boolean)


744
745
746
747
748
# File 'app/models/merge_request.rb', line 744

def supports_lock_on_merge?
  return false unless merged?

  project.supports_lock_on_merge?
end

#supports_suggestion?Boolean

Returns:

  • (Boolean)


740
741
742
# File 'app/models/merge_request.rb', line 740

def supports_suggestion?
  true
end

#target_branch_exists?Boolean

Returns:

  • (Boolean)


1450
1451
1452
1453
1454
# File 'app/models/merge_request.rb', line 1450

def target_branch_exists?
  return false unless self.target_project

  self.target_project.repository.branch_exists?(self.target_branch)
end

#target_branch_headObject



953
954
955
956
957
# File 'app/models/merge_request.rb', line 953

def target_branch_head
  strong_memoize(:target_branch_head) do
    target_project.repository.commit(target_branch_ref)
  end
end

#target_branch_refObject



938
939
940
941
942
943
# File 'app/models/merge_request.rb', line 938

def target_branch_ref
  return @target_branch_sha if @target_branch_sha
  return unless target_branch

  Gitlab::Git::BRANCH_REF_PREFIX + target_branch
end

#target_default_branch?Boolean

Returns:

  • (Boolean)


2076
2077
2078
# File 'app/models/merge_request.rb', line 2076

def target_default_branch?
  target_branch == project.default_branch
end

#target_project_namespaceObject



1436
1437
1438
1439
1440
1441
1442
# File 'app/models/merge_request.rb', line 1436

def target_project_namespace
  if target_project && target_project.namespace
    target_project.namespace.full_path
  else
    "(removed)"
  end
end

#target_project_pathObject



1412
1413
1414
1415
1416
1417
1418
# File 'app/models/merge_request.rb', line 1412

def target_project_path
  if target_project
    target_project.full_path
  else
    "(removed)"
  end
end

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

‘from` argument can be a Namespace or Project.



682
683
684
685
686
# File 'app/models/merge_request.rb', line 682

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

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

#train_ref_pathObject



1559
1560
1561
# File 'app/models/merge_request.rb', line 1559

def train_ref_path
  "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/train"
end

#update_and_mark_in_progress_merge_commit_sha(commit_id) ⇒ Object



1604
1605
1606
1607
1608
1609
1610
# File 'app/models/merge_request.rb', line 1604

def update_and_mark_in_progress_merge_commit_sha(commit_id)
  self.update(in_progress_merge_commit_sha: commit_id)
  # Since another process checks for matching merge request, we need
  # to make it possible to detect whether the query should go to the
  # primary.
  target_project.sticking.stick(:project, target_project.id)
end

#update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil) ⇒ Object

rubocop: disable CodeReuse/ServiceClass



1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
# File 'app/models/merge_request.rb', line 1893

def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
  return unless has_complete_diff_refs?
  return if new_diff_refs == old_diff_refs

  active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
    discussion.active?(old_diff_refs)
  end
  return if active_diff_discussions.empty?

  paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq

  active_discussions_resolved = active_diff_discussions.all?(&:resolved?)

  service = Discussions::UpdateDiffPositionService.new(
    self.project,
    current_user,
    old_diff_refs: old_diff_refs,
    new_diff_refs: new_diff_refs,
    paths: paths
  )

  active_diff_discussions.each do |discussion|
    service.execute(discussion)
    discussion.clear_memoized_values
  end

  # If they were all already resolved, this method will have already been called.
  # If they all don't get resolved, we don't need to call the method
  # If they go from unresolved -> resolved, then we call the method
  if !active_discussions_resolved &&
      active_diff_discussions.all?(&:resolved?) &&
      project.resolve_outdated_diff_discussions?
    MergeRequests::ResolvedDiscussionNotificationService
      .new(project: project, current_user: current_user)
      .execute(self)
  end
end

#update_head_pipelineObject



1645
1646
1647
1648
1649
1650
# File 'app/models/merge_request.rb', line 1645

def update_head_pipeline
  find_actual_head_pipeline.try do |pipeline|
    self.head_pipeline = pipeline
    update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed?
  end
end

#update_project_counter_cachesObject

rubocop: disable CodeReuse/ServiceClass



1976
1977
1978
# File 'app/models/merge_request.rb', line 1976

def update_project_counter_caches
  Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end

#use_merge_base_pipeline_for_comparison?(_) ⇒ Boolean

Overridden in EE

Returns:

  • (Boolean)


1951
1952
1953
# File 'app/models/merge_request.rb', line 1951

def use_merge_base_pipeline_for_comparison?(_)
  false
end

#validate_branch_name(attr) ⇒ Object



1041
1042
1043
1044
1045
1046
1047
1048
1049
# File 'app/models/merge_request.rb', line 1041

def validate_branch_name(attr)
  return unless will_save_change_to_attribute?(attr)

  branch = read_attribute(attr)

  return unless branch

  errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch)
end

#validate_branchesObject



1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
# File 'app/models/merge_request.rb', line 1012

def validate_branches
  return unless target_project && source_project

  if target_project == source_project && target_branch == source_branch
    errors.add :branch_conflict, "You can't use same project/branch for source and target"
    return
  end

  [:source_branch, :target_branch].each { |attr| validate_branch_name(attr) }

  if opened?
    conflicting_mr = existing_mrs_targeting_same_branch.first

    if conflicting_mr
      errors.add(
        :validate_branches,
        conflicting_mr_message(conflicting_mr)
      )
    end
  end
end

#validate_forkObject



1057
1058
1059
1060
1061
1062
1063
# File 'app/models/merge_request.rb', line 1057

def validate_fork
  return true unless target_project && source_project
  return true if target_project == source_project
  return true unless source_project_missing?

  errors.add :validate_fork, 'Source project is not a fork of the target project'
end

#validate_reviewer_size_lengthObject



1065
1066
1067
1068
1069
1070
# File 'app/models/merge_request.rb', line 1065

def validate_reviewer_size_length
  return true unless reviewers.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS

  errors.add :reviewers,
    -> (_object, _data) { self.class.max_number_of_assignees_or_reviewers_message }
end

#validate_target_projectObject



1051
1052
1053
1054
1055
# File 'app/models/merge_request.rb', line 1051

def validate_target_project
  return true if target_project.merge_requests_enabled?

  errors.add :base, 'Target project has disabled merge requests'
end

#version_params_for(diff_refs) ⇒ Object



1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
# File 'app/models/merge_request.rb', line 1150

def version_params_for(diff_refs)
  if diff = merge_request_diff_for(diff_refs)
    { diff_id: diff.id }
  elsif diff = merge_request_diff_for(diff_refs.head_sha)
    {
      diff_id: diff.id,
      start_sha: diff_refs.start_sha
    }
  end
end

#viewable_diffsObject



1129
1130
1131
# File 'app/models/merge_request.rb', line 1129

def viewable_diffs
  @viewable_diffs ||= merge_request_diffs.viewable.to_a
end

#visible_closing_issues_for(current_user = self.author) ⇒ Object



1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
# File 'app/models/merge_request.rb', line 1378

def visible_closing_issues_for(current_user = self.author)
  strong_memoize(:visible_closing_issues_for) do
    if self.target_project.has_external_issue_tracker?
      closes_issues(current_user)
    else
      cached_closes_issues.select do |issue|
        Ability.allowed?(current_user, :read_issue, issue)
      end
    end
  end
end