Class: MergeRequest

Defined Under Namespace

Classes: ApprovalMetrics, ApprovalRemovalSettings, CleanupSchedule, CommitsMetadata, DiffCommitUser, Metrics, MetricsFinder

Constant Summary collapse

SORTING_PREFERENCE_FIELD =
:merge_requests_sort
CI_MERGE_REQUEST_DESCRIPTION_MAX_LENGTH =
2700
MERGE_LEASE_TIMEOUT =
15.minutes.to_i
DIFF_VERSION_LIMIT =
1_000
DIFF_COMMITS_LIMIT =
1_000_000
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_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 collapse

Attributes included from Gitlab::Cache::RequestCache

#request_cache_key_block

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::Cache::RequestCache

extended, request_cache, request_cache_key

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

#approvals_for_user_ids, #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

#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 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_move?, #card_attributes, #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, #resource_parent, #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

#as_json, #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, #gfm_reference, #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

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



184
185
186
# File 'app/models/merge_request.rb', line 184

def allow_broken
  @allow_broken
end

#can_be_createdObject

Temporary fields to store compare vars when creating new merge request



192
193
194
# File 'app/models/merge_request.rb', line 192

def can_be_created
  @can_be_created
end

#compareObject

Temporary fields to store compare vars when creating new merge request



192
193
194
# File 'app/models/merge_request.rb', line 192

def compare
  @compare
end

#compare_commitsObject

Temporary fields to store compare vars when creating new merge request



192
193
194
# File 'app/models/merge_request.rb', line 192

def compare_commits
  @compare_commits
end

#diff_optionsObject

Temporary fields to store compare vars when creating new merge request



192
193
194
# File 'app/models/merge_request.rb', line 192

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



188
189
190
# File 'app/models/merge_request.rb', line 188

def skip_ensure_merge_request_diff
  @skip_ensure_merge_request_diff
end

#skip_merge_status_triggerObject

Flag to skip triggering mergeRequestMergeStatusUpdated GraphQL subscription.



195
196
197
# File 'app/models/merge_request.rb', line 195

def skip_merge_status_trigger
  @skip_merge_status_trigger
end

#source_branch_shaObject



1243
1244
1245
# File 'app/models/merge_request.rb', line 1243

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

#target_branch_shaObject



1239
1240
1241
# File 'app/models/merge_request.rb', line 1239

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

Class Method Details

.all_mergeability_checksObject



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

def self.all_mergeability_checks
  mergeable_state_checks + mergeable_git_state_checks
end

.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.


206
207
208
# File 'app/models/merge_request.rb', line 206

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

.distinct_source_branchesObject



748
749
750
# File 'app/models/merge_request.rb', line 748

def self.distinct_source_branches
  distinct.pluck(:source_branch)
end

.draft?(title) ⇒ Boolean

Returns:

  • (Boolean)


863
864
865
# File 'app/models/merge_request.rb', line 863

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

.draft_title(title) ⇒ Object



871
872
873
# File 'app/models/merge_request.rb', line 871

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

.draftless_title(title) ⇒ Object



867
868
869
# File 'app/models/merge_request.rb', line 867

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.



832
833
834
835
836
837
838
839
# File 'app/models/merge_request.rb', line 832

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


810
811
812
# File 'app/models/merge_request.rb', line 810

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)


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

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

.merge_train_ref?(ref) ⇒ Boolean

Returns:

  • (Boolean)


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

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

.mergeable_git_state_checksObject



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

def self.mergeable_git_state_checks
  [
    ::MergeRequests::Mergeability::CheckConflictStatusService,
    ::MergeRequests::Mergeability::CheckRebaseStatusService
  ]
end

.mergeable_state_checksObject



1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
# File 'app/models/merge_request.rb', line 1529

def self.mergeable_state_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::CheckMergeTimeService,
    ::MergeRequests::Mergeability::CheckDraftStatusService,
    ::MergeRequests::Mergeability::CheckCommitsStatusService,
    ::MergeRequests::Mergeability::CheckDiscussionsStatusService,
    ::MergeRequests::Mergeability::CheckMergeRequestTitleRegexService,
    ::MergeRequests::Mergeability::CheckCiStatusService,
    ::MergeRequests::Mergeability::CheckLfsFileLocksService
  ]
end

.order_by_metric_column(metric, direction) ⇒ Object



701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
# File 'app/models/merge_request.rb', line 701

def self.order_by_metric_column(metric, direction)
  column_expression = MergeRequest::Metrics.arel_table[metric]
  column_expression_with_direction = direction == 'ASC' ? column_expression.asc : column_expression.desc

  Gitlab::Pagination::Keyset::Order.build(
    [
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: "merge_request_metrics_#{metric}",
        column_expression: column_expression,
        order_expression: column_expression_with_direction.nulls_last,
        reversed_order_expression: column_expression_with_direction.reverse.nulls_first,
        order_direction: direction,
        nullable: :nulls_last,
        add_to_projections: true
      ),
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: 'merge_request_metrics_id',
        order_expression: MergeRequest::Metrics.arel_table[:id].desc,
        add_to_projections: true
      )
    ])
end

.participant_includesObject



881
882
883
# File 'app/models/merge_request.rb', line 881

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

.project_foreign_keyObject



818
819
820
# File 'app/models/merge_request.rb', line 818

def self.project_foreign_key
  'target_project_id'
end

.recent_source_branches(limit: 100) ⇒ Object



740
741
742
743
744
745
746
# File 'app/models/merge_request.rb', line 740

def self.recent_source_branches(limit: 100)
  group(:source_branch)
    .select(:source_branch)
    .reorder(arel_table[:updated_at].maximum.desc)
    .limit(limit)
    .pluck(:source_branch)
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.



732
733
734
735
736
737
738
# File 'app/models/merge_request.rb', line 732

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.



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

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

.reference_prefixObject



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

def self.reference_prefix
  '!'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)


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

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

.reviewers_subqueryObject



763
764
765
766
767
# File 'app/models/merge_request.rb', line 763

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`.



847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'app/models/merge_request.rb', line 847

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



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

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



669
670
671
672
673
674
675
676
677
678
# File 'app/models/merge_request.rb', line 669

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

.use_locked_set?Boolean

Returns:

  • (Boolean)


885
886
887
# File 'app/models/merge_request.rb', line 885

def self.use_locked_set?
  Feature.enabled?(:unstick_locked_merge_requests_redis) # rubocop:disable Gitlab/FeatureFlagWithoutActor -- pre-existing feature flag
end

.wip_titleObject



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

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

.wipless_titleObject



877
878
879
# File 'app/models/merge_request.rb', line 877

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

.work_in_progress?Boolean

Returns:

  • (Boolean)


876
877
878
# File 'app/models/merge_request.rb', line 876

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

Instance Method Details

#add_to_locked_setObject



2658
2659
2660
2661
2662
# File 'app/models/merge_request.rb', line 2658

def add_to_locked_set
  return unless self.class.use_locked_set?

  Gitlab::MergeRequests::LockedSet.add(self.id, rescue_connection_error: false)
end

#all_commit_shasObject

Note that this could also return SHA from now dangling commits



2272
2273
2274
2275
2276
2277
2278
2279
2280
# File 'app/models/merge_request.rb', line 2272

def all_commit_shas
  return commit_shas unless persisted?

  if Feature.enabled?(:merge_request_diff_commits_dedup, project)
    
  else
    all_commits.pluck(:sha).uniq
  end
end

#all_commitsObject



2264
2265
2266
2267
2268
# File 'app/models/merge_request.rb', line 2264

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

#all_mergeability_checks_resultsObject



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

def all_mergeability_checks_results
  execute_merge_checks(
    self.class.all_mergeability_checks,
    execute_all: true
  ).payload[:results]
end

#all_pipelinesObject



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

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

#allow_collaborationObject Also known as: allow_collaboration?



2467
2468
2469
# File 'app/models/merge_request.rb', line 2467

def allow_collaboration
  collaborative_push_possible? && allow_maintainer_to_push
end

#allow_merge_without_pipeline?Boolean

Returns:

  • (Boolean)


1923
1924
1925
# File 'app/models/merge_request.rb', line 1923

def allow_merge_without_pipeline?
  project.allow_merge_without_pipeline?(inherit_group_setting: true)
end

#allows_multiple_assignees?Boolean

Returns:

  • (Boolean)


2509
2510
2511
# File 'app/models/merge_request.rb', line 2509

def allows_multiple_assignees?
  project.allows_multiple_merge_request_assignees?
end

#allows_multiple_reviewers?Boolean

Returns:

  • (Boolean)


2513
2514
2515
# File 'app/models/merge_request.rb', line 2513

def allows_multiple_reviewers?
  project.allows_multiple_merge_request_reviewers?
end

#allows_reviewers?Boolean

Returns:

  • (Boolean)


2505
2506
2507
# File 'app/models/merge_request.rb', line 2505

def allows_reviewers?
  true
end

#append_merge_params(params) ⇒ Object



2768
2769
2770
2771
2772
2773
# File 'app/models/merge_request.rb', line 2768

def append_merge_params(params)
  with_merge_data_sync(
    -> { merge_params.merge!(params) },
    -> { merge_data.merge_params.merge!(params) }
  )
end

#async_cleanup_refs(only: :all) ⇒ Object



1996
1997
1998
# File 'app/models/merge_request.rb', line 1996

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

#auto_merge_strategyObject



1626
1627
1628
1629
1630
# File 'app/models/merge_request.rb', line 1626

def auto_merge_strategy
  return unless auto_merge_enabled?

  merge_params['auto_merge_strategy'] || default_auto_merge_strategy
end

#auto_merge_strategy=(strategy) ⇒ Object



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

def auto_merge_strategy=(strategy)
  append_merge_params({ 'auto_merge_strategy' => strategy })
end

#banzai_render_context(field) ⇒ Object



2497
2498
2499
# File 'app/models/merge_request.rb', line 2497

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

#base_pipelineObject



2442
2443
2444
# File 'app/models/merge_request.rb', line 2442

def base_pipeline
  target_branch_pipelines_for(sha: diff_base_sha).last
end

#batch_update_reviewer_state(user_ids, state) ⇒ Object



2541
2542
2543
# File 'app/models/merge_request.rb', line 2541

def batch_update_reviewer_state(user_ids, state)
  merge_request_reviewers.where(user_id: user_ids).update_all(state: state)
end

#branch_merge_base_commitObject



1230
1231
1232
1233
1234
1235
1236
1237
# File 'app/models/merge_request.rb', line 1230

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



1267
1268
1269
# File 'app/models/merge_request.rb', line 1267

def branch_merge_base_sha
  branch_merge_base_commit.try(:sha)
end

#branch_missing?Boolean

Returns:

  • (Boolean)


1906
1907
1908
# File 'app/models/merge_request.rb', line 1906

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

#broken?Boolean

Returns:

  • (Boolean)


1910
1911
1912
# File 'app/models/merge_request.rb', line 1910

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.



1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
# File 'app/models/merge_request.rb', line 1700

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

  issues_to_close_ids = closes_issues(current_user).reject { |issue| issue.is_a?(ExternalIssue) }.map(&:id)

  transaction do
    merge_requests_closing_issues.from_mr_description.delete_all

    # These might have been created manually from the work item interface
    issue_ids_to_update = merge_requests_closing_issues
      .where(from_mr_description: false, issue_id: issues_to_close_ids)
      .pluck(:issue_id)

    if issue_ids_to_update.any?
      merge_requests_closing_issues.where(issue_id: issue_ids_to_update).update_all(from_mr_description: true)
    end

    issue_ids_to_create = issues_to_close_ids - issue_ids_to_update
    next unless issue_ids_to_create.any?

    now = Time.zone.now
    new_associations = issue_ids_to_create.map do |issue_id|
      MergeRequestsClosingIssues.new(
        issue_id: issue_id,
        merge_request_id: id,
        from_mr_description: true,
        created_at: now,
        updated_at: now
      )
    end

    # We can't skip validations here in bulk insert as we don't have a unique constraint on the DB.
    # We can skip validations once we have validated the unique constraint
    # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/456965
    MergeRequestsClosingIssues.bulk_insert!(new_associations, batch_size: 100)
  end
end

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

Raises:

  • (NameError)


2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
# File 'app/models/merge_request.rb', line 2244

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), diff_head_pipeline)
end

#can_allow_collaboration?(user) ⇒ Boolean

Returns:

  • (Boolean)


2480
2481
2482
2483
# File 'app/models/merge_request.rb', line 2480

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

#can_be_cherry_picked?Boolean

Returns:

  • (Boolean)


2353
2354
2355
# File 'app/models/merge_request.rb', line 2353

def can_be_cherry_picked?
  commit_to_cherry_pick.present?
end

#can_be_closed?Boolean

Returns:

  • (Boolean)


1366
1367
1368
# File 'app/models/merge_request.rb', line 1366

def can_be_closed?
  opened?
end

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

Returns:

  • (Boolean)


1914
1915
1916
1917
# File 'app/models/merge_request.rb', line 1914

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_reverted?(current_user) ⇒ Boolean

Returns:

  • (Boolean)


2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
# File 'app/models/merge_request.rb', line 2325

def can_be_reverted?(current_user)
  return false unless merged_at

  commit = commit_to_revert
  return false unless commit

  # 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)

  !commit.has_been_reverted?(current_user, notes_association)
end

#can_cancel_auto_merge?(current_user) ⇒ Boolean

Returns:

  • (Boolean)


1606
1607
1608
# File 'app/models/merge_request.rb', line 1606

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)


1610
1611
1612
1613
1614
1615
1616
# File 'app/models/merge_request.rb', line 1610

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)


2580
2581
2582
# File 'app/models/merge_request.rb', line 2580

def can_suggest_reviewers?
  false # overridden in EE
end

#changed_pathsObject



1139
1140
1141
# File 'app/models/merge_request.rb', line 1139

def changed_paths
  project.repository.find_changed_paths(commits, merge_commit_diff_mode: :all_parents)
end

#check_for_spam?Boolean

Returns:

  • (Boolean)


2596
2597
2598
# File 'app/models/merge_request.rb', line 2596

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

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



1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
# File 'app/models/merge_request.rb', line 1445

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



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

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

#clear_memoized_shasObject



1423
1424
1425
1426
1427
1428
1429
# File 'app/models/merge_request.rb', line 1423

def clear_memoized_shas
  @target_branch_sha = @source_branch_sha = nil

  clear_memoization(:source_branch_head)
  clear_memoization(:target_branch_head)
  clear_memoization(:diff_stats)
end

#clear_merge_params(param_keys) ⇒ Object



2775
2776
2777
2778
2779
2780
# File 'app/models/merge_request.rb', line 2775

def clear_merge_params(param_keys)
  with_merge_data_sync(
    -> { merge_params.except!(*param_keys) },
    -> { merge_data.merge_params.except!(*param_keys) }
  )
end

#closed_eventObject



1472
1473
1474
# File 'app/models/merge_request.rb', line 1472

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)


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

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.



1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
# File 'app/models/merge_request.rb', line 1760

def closes_issues(current_user = self.author)
  if target_branch == project.default_branch
    messages = [title, description]
    messages.concat(commits(load_from_gitaly: true).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)


2473
2474
2475
2476
2477
2478
# File 'app/models/merge_request.rb', line 2473

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_exists?(sha) ⇒ Boolean

Returns:

  • (Boolean)


2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
# File 'app/models/merge_request.rb', line 2721

def commit_exists?(sha)
  return all_commits.exists?(sha: sha) if Feature.disabled?(:merge_request_diff_commits_dedup, project)

  # We query the SHA from `merge_request_commits_metadata` table first and
  # fallback to querying them from `merge_request_diff_commits` if doesn't match
  # anything. That is to check if commit exists but the records are old and there
  # are no `merge_request_commits_metadata_id` set for them since they're not
  # backfilled yet.
  return true if MergeRequest::CommitsMetadata
    .where(project: project, sha: sha)
    .where_exists(
      MergeRequestDiffCommit
        .where('merge_request_diff_commits.merge_request_commits_metadata_id = merge_request_commits_metadata.id')
        .where_exists(
          merge_request_diffs.where('merge_request_diffs.id = merge_request_diff_commits.merge_request_diff_id')
        )
    )
    .exists?

  all_commits.exists?(sha: sha)
end

#commit_notesObject



1667
1668
1669
1670
1671
1672
1673
1674
1675
# File 'app/models/merge_request.rb', line 1667

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



975
976
977
978
979
980
981
982
983
984
985
986
# File 'app/models/merge_request.rb', line 975

def commit_shas(limit: nil)
  return merge_request_diff.commit_shas(limit: limit, preload_metadata: true) 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

#commit_to_cherry_pickObject



2321
2322
2323
# File 'app/models/merge_request.rb', line 2321

def commit_to_cherry_pick
  commit_to_revert
end

#commit_to_revertObject

Exists only for merged merge requests



2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
# File 'app/models/merge_request.rb', line 2305

def commit_to_revert
  return unless merged?

  # By default, it's equal to a merge commit
  return merge_commit if merge_commit

  # But in case of fast-forward merge merge commits are not created
  # To solve that we can use `squash_commit` if the merge request was squashed
  return squash_commit if squash_commit

  # Edge case: one commit in the merge request without merge or squash commit
  return project.commit(diff_head_sha) if commits_count == 1

  nil
end

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



952
953
954
955
956
957
958
959
960
961
962
963
# File 'app/models/merge_request.rb', line 952

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



965
966
967
968
969
970
971
972
973
# File 'app/models/merge_request.rb', line 965

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, lazy: false, include_author_when_signed: false) ⇒ Object



908
909
910
911
912
913
914
915
916
917
918
919
920
# File 'app/models/merge_request.rb', line 908

def committers(with_merge_commits: false, lazy: false, include_author_when_signed: false)
  strong_memoize_with(:committers, with_merge_commits, lazy, include_author_when_signed) do
    if Feature.enabled?(:merge_request_diff_commits_dedup, project)
      
    end

    commits.committers(
      with_merge_commits: with_merge_commits,
      lazy: lazy,
      include_author_when_signed: include_author_when_signed
    )
  end
end

#compare_accessibility_reportsObject



2128
2129
2130
2131
2132
2133
2134
# File 'app/models/merge_request.rb', line 2128

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



2168
2169
2170
2171
2172
2173
2174
# File 'app/models/merge_request.rb', line 2168

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



2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
# File 'app/models/merge_request.rb', line 2203

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), diff_head_pipeline, data)
      raise InvalidateReactiveCache
    end

    data
  end || { status: :parsing }
end

#compare_sast_reports(current_user) ⇒ Object



2232
2233
2234
2235
2236
# File 'app/models/merge_request.rb', line 2232

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

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

#compare_secret_detection_reports(current_user) ⇒ Object



2238
2239
2240
2241
2242
# File 'app/models/merge_request.rb', line 2238

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

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

#compare_test_reportsObject

rubocop: enable Metrics/AbcSize



2108
2109
2110
2111
2112
2113
2114
# File 'app/models/merge_request.rb', line 2108

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



2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
# File 'app/models/merge_request.rb', line 2419

def comparison_base_pipeline(service_class)
  target_shas = [
    diff_head_pipeline&.target_sha,
    diff_base_sha,
    diff_start_sha
  ]

  target_shas
    .compact
    .lazy
    .filter_map { |sha| target_branch_pipelines_for(sha: sha).last }
    .first
end

#conflicting_mr_message(conflicting_mr) ⇒ Object



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

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



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

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



948
949
950
# File 'app/models/merge_request.rb', line 948

def context_commits_count
  context_commits.count
end

#context_commits_diffObject



2558
2559
2560
2561
2562
# File 'app/models/merge_request.rb', line 2558

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

#create_merge_request_diff(preload_gitaly: false) ⇒ Object



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

def create_merge_request_diff(preload_gitaly: false)
  fetch_ref!

  # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377
  Gitlab::GitalyClient.allow_n_plus_1_calls do
    if preload_gitaly
      new_diff = merge_request_diffs.build
      new_diff.preload_gitaly_data
      new_diff.save!
    else
      merge_request_diffs.create!
    end

    reload_merge_request_diff
  end
end

#create_requested_changes(user) ⇒ Object



2533
2534
2535
# File 'app/models/merge_request.rb', line 2533

def create_requested_changes(user)
  # Overridden in EE
end

#current_patch_id_shaObject



2604
2605
2606
# File 'app/models/merge_request.rb', line 2604

def current_patch_id_sha
  merge_request_diff.get_patch_id_sha
end

#default_auto_merge_strategyObject



1632
1633
1634
# File 'app/models/merge_request.rb', line 1632

def default_auto_merge_strategy
  AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS
end

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



1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
# File 'app/models/merge_request.rb', line 1848

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



1872
1873
1874
1875
1876
# File 'app/models/merge_request.rb', line 1872

def default_squash_commit_message(user: nil)
  squash_commit_message = ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).squash_commit_message.presence

  squash_commit_message || title
end

#destroy_requested_changes(user) ⇒ Object



2537
2538
2539
# File 'app/models/merge_request.rb', line 2537

def destroy_requested_changes(user)
  # Overridden in EE
end

#diff_base_commitObject



1148
1149
1150
1151
1152
1153
1154
# File 'app/models/merge_request.rb', line 1148

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

#diff_base_shaObject



1180
1181
1182
1183
1184
1185
1186
# File 'app/models/merge_request.rb', line 1180

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



1164
1165
1166
1167
1168
1169
1170
# File 'app/models/merge_request.rb', line 1164

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

#diff_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



782
783
784
# File 'app/models/merge_request.rb', line 782

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

#diff_head_pipeline_considered_in_progress?Boolean

Returns:

  • (Boolean)


2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
# File 'app/models/merge_request.rb', line 2608

def diff_head_pipeline_considered_in_progress?
  return true if pipeline_creating?

  pipeline = diff_head_pipeline
  return false unless pipeline

  # We allow auto-merge on blocked pipelines when "Pipelines must succeed" is
  # enabled, because in that case the pipeline blocks the merge. When
  # "Pipelines must succeed" is disabled, immediate merges are neither blocked
  # nor discouraged on blocked pipelines, so auto merge should not wait for
  # the pipeline to finish.
  if only_allow_merge_if_pipeline_succeeds?
    !pipeline.complete?
  else
    pipeline.active? || pipeline.created?
  end
end

#diff_head_pipeline_success?Boolean

Returns:

  • (Boolean)


796
797
798
# File 'app/models/merge_request.rb', line 796

def diff_head_pipeline_success?
  !!diff_head_pipeline&.success?
end

#diff_head_shaObject



1188
1189
1190
1191
1192
1193
1194
# File 'app/models/merge_request.rb', line 1188

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



1247
1248
1249
1250
1251
1252
1253
# File 'app/models/merge_request.rb', line 1247

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

#diff_sizeObject



1123
1124
1125
1126
1127
# File 'app/models/merge_request.rb', line 1123

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



1156
1157
1158
1159
1160
1161
1162
# File 'app/models/merge_request.rb', line 1156

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

#diff_start_shaObject



1172
1173
1174
1175
1176
1177
1178
# File 'app/models/merge_request.rb', line 1172

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



1115
1116
1117
1118
1119
1120
1121
# File 'app/models/merge_request.rb', line 1115

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)


1458
1459
1460
# File 'app/models/merge_request.rb', line 1458

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

#diffs(diff_options = {}) ⇒ Object



1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
# File 'app/models/merge_request.rb', line 1068

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_keyObject



2714
2715
2716
2717
2718
2719
# File 'app/models/merge_request.rb', line 2714

def diffs_batch_cache_key
  return unless diffs_batch_cache_with_max_age?
  return latest_merge_request_diff&.patch_id_sha if cannot_be_merged?

  merge_head_diff&.patch_id_sha
end

#diffs_batch_cache_with_max_age?Boolean

Returns:

  • (Boolean)


2588
2589
2590
# File 'app/models/merge_request.rb', line 2588

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

#diffs_for_streaming(diff_options = {}) ⇒ Object



1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
# File 'app/models/merge_request.rb', line 1050

def diffs_for_streaming(diff_options = {}, &)
  diff = diffable_merge_ref? ? merge_head_diff : merge_request_diff

  offset = diff_options[:offset_index].to_i || 0

  if block_given?
    source_project.repository.diffs_by_changed_paths(diff.diff_refs, offset, &)
  else
    diff.diffs_for_streaming(diff_options)
  end
end

#discussions_diffsObject



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

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)


2447
2448
2449
# File 'app/models/merge_request.rb', line 2447

def discussions_rendered_on_frontend?
  true
end

#diverged_commits_countObject



2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
# File 'app/models/merge_request.rb', line 2032

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)


2055
2056
2057
# File 'app/models/merge_request.rb', line 2055

def diverged_from_target_branch?
  diverged_commits_count > 0
end

#draft?Boolean Also known as: work_in_progress?

Returns:

  • (Boolean)


1476
1477
1478
# File 'app/models/merge_request.rb', line 1476

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

#draft_titleObject Also known as: wip_title



1486
1487
1488
# File 'app/models/merge_request.rb', line 1486

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

#draftless_titleObject Also known as: wipless_title



1481
1482
1483
# File 'app/models/merge_request.rb', line 1481

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.



924
925
926
# File 'app/models/merge_request.rb', line 924

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

#duo_code_review_progress_noteObject



1677
1678
1679
# File 'app/models/merge_request.rb', line 1677

def duo_code_review_progress_note
  # Overridden in EE
end

#enabled_reportsObject



2545
2546
2547
2548
2549
2550
# File 'app/models/merge_request.rb', line 2545

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

#ensure_merge_dataObject



2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
# File 'app/models/merge_request.rb', line 2810

def ensure_merge_data
  # Eventually we probably need to set up a callback for this, but we need to
  #   conditionally initialize this for now.
  record = merge_data || build_merge_data(merge_data_attributes)

  # id may not have been set if the merge_request got saved after the initial
  #   ensure_merge_data so we need to set merge_request_id here if it is not already set.
  record.merge_request_id ||= id
  record.project_id ||= target_project_id
  record
end

#ensure_merge_request_diffObject



1370
1371
1372
# File 'app/models/merge_request.rb', line 1370

def ensure_merge_request_diff
  merge_request_diff.persisted? || create_merge_request_diff
end

#ensure_metrics!Object



2501
2502
2503
# File 'app/models/merge_request.rb', line 2501

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

#environments_in_head_pipeline(deployment_status: nil) ⇒ Object



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

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

#execute_merge_checks(checks, params: {}, execute_all: false) ⇒ Object



2572
2573
2574
2575
2576
2577
2578
# File 'app/models/merge_request.rb', line 2572

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

#existing_mrs_targeting_same_branchObject



1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
# File 'app/models/merge_request.rb', line 1271

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



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

def fetch_ref!
  target_project.repository.fetch_source_branch!(source_project.repository, source_branch_ref(or_sha: false), ref_path)
  expire_ancestor_cache
end

#ff_merge_possible?Boolean

Returns:

  • (Boolean)


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

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

#find_assignee(user) ⇒ Object



2521
2522
2523
# File 'app/models/merge_request.rb', line 2521

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



2156
2157
2158
2159
2160
2161
2162
# File 'app/models/merge_request.rb', line 2156

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



2140
2141
2142
2143
2144
2145
2146
# File 'app/models/merge_request.rb', line 2140

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_diff_head_pipelineObject



2485
2486
2487
# File 'app/models/merge_request.rb', line 2485

def find_diff_head_pipeline
  all_pipelines.for_sha_or_source_sha(diff_head_sha).first
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



2192
2193
2194
2195
2196
2197
2198
# File 'app/models/merge_request.rb', line 2192

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



2525
2526
2527
# File 'app/models/merge_request.rb', line 2525

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

#find_terraform_reportsObject



2176
2177
2178
2179
2180
2181
2182
# File 'app/models/merge_request.rb', line 2176

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



1042
1043
1044
# File 'app/models/merge_request.rb', line 1042

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

#first_contribution?Boolean

rubocop: enable CodeReuse/ServiceClass

Returns:

  • (Boolean)


2457
2458
2459
2460
2461
# File 'app/models/merge_request.rb', line 2457

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

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

#first_diffs_slice(limit, diff_options = {}) ⇒ Object



2670
2671
2672
2673
# File 'app/models/merge_request.rb', line 2670

def first_diffs_slice(limit, diff_options = {})
  diff = diffable_merge_ref? ? merge_head_diff : merge_request_diff
  diff.paginated_diffs(1, limit, diff_options).diff_files(sorted: true)
end

#first_multiline_commitObject

Returns the oldest multi-line commit



1879
1880
1881
1882
1883
# File 'app/models/merge_request.rb', line 1879

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

#first_multiline_commit_descriptionObject

Returns the description (without the first line/title) of the first multiline commit



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

def first_multiline_commit_description
  strong_memoize(:first_multiline_commit_description) do
    commit = first_multiline_commit
    commit&.description
  end
end

#for_fork?Boolean

Returns:

  • (Boolean)


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

def for_fork?
  target_project != source_project
end

#for_same_project?Boolean

Returns:

  • (Boolean)


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

def for_same_project?
  target_project == source_project
end

#force_remove_source_branch?Boolean

Returns:

  • (Boolean)


1622
1623
1624
# File 'app/models/merge_request.rb', line 1622

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

#has_accessibility_reports?Boolean

Returns:

  • (Boolean)


2116
2117
2118
# File 'app/models/merge_request.rb', line 2116

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

#has_ci?Boolean

Returns:

  • (Boolean)


1900
1901
1902
1903
1904
# File 'app/models/merge_request.rb', line 1900

def has_ci?
  return false if has_no_commits?

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

#has_ci_enabled?Boolean

We use a heuristic of if there are pipeline created, being created, or a ci integration is setup

Returns:

  • (Boolean)


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

def has_ci_enabled?
  has_ci? || pipeline_creating?
end

#has_codequality_mr_diff_report?Boolean

Returns:

  • (Boolean)


2148
2149
2150
# File 'app/models/merge_request.rb', line 2148

def has_codequality_mr_diff_report?
  diff_head_pipeline&.has_codequality_mr_diff_report?
end

#has_codequality_reports?Boolean

Returns:

  • (Boolean)


2164
2165
2166
# File 'app/models/merge_request.rb', line 2164

def has_codequality_reports?
  !!diff_head_pipeline&.complete_and_has_self_or_descendant_reports?(Ci::JobArtifact.of_report_type(:codequality))
end

#has_commits?Boolean

Returns:

  • (Boolean)


2405
2406
2407
# File 'app/models/merge_request.rb', line 2405

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

#has_complete_diff_refs?Boolean

Returns:

  • (Boolean)


2357
2358
2359
# File 'app/models/merge_request.rb', line 2357

def has_complete_diff_refs?
  diff_refs && diff_refs.complete?
end

#has_coverage_reports?Boolean

Returns:

  • (Boolean)


2120
2121
2122
# File 'app/models/merge_request.rb', line 2120

def has_coverage_reports?
  diff_head_pipeline&.has_coverage_reports?
end

#has_diffs?Boolean

Returns:

  • (Boolean)


2650
2651
2652
2653
2654
2655
2656
# File 'app/models/merge_request.rb', line 2650

def has_diffs?
  Gitlab::Git::Compare.new(
    project.repository.raw_repository,
    target_branch_sha,
    source_branch_sha
  ).diffs.any?
end

#has_exposed_artifacts?Boolean

Returns:

  • (Boolean)


2184
2185
2186
# File 'app/models/merge_request.rb', line 2184

def has_exposed_artifacts?
  diff_head_pipeline&.has_exposed_artifacts?
end

#has_jira_issue_keys?Boolean

Returns:

  • (Boolean)


2630
2631
2632
2633
2634
2635
2636
2637
2638
# File 'app/models/merge_request.rb', line 2630

def has_jira_issue_keys?
  return false unless project&.jira_integration&.reference_pattern

  Atlassian::JiraIssueKeyExtractor.has_keys?(
    title,
    description,
    custom_regex: project.jira_integration.reference_pattern
  )
end

#has_no_commits?Boolean

Returns:

  • (Boolean)


2409
2410
2411
# File 'app/models/merge_request.rb', line 2409

def has_no_commits?
  !has_commits?
end

#has_sast_reports?Boolean

Returns:

  • (Boolean)


2214
2215
2216
2217
2218
2219
2220
2221
# File 'app/models/merge_request.rb', line 2214

def has_sast_reports?
  if Feature.enabled?(:show_child_security_reports_in_mr_widget, project)
    !!diff_head_pipeline&.complete_or_manual? &&
      pipeline_has_report_in_self_or_descendants?(:sast)
  else
    !!diff_head_pipeline&.complete_or_manual_and_has_reports?(::Ci::JobArtifact.of_report_type(:sast))
  end
end

#has_secret_detection_reports?Boolean

Returns:

  • (Boolean)


2223
2224
2225
2226
2227
2228
2229
2230
# File 'app/models/merge_request.rb', line 2223

def has_secret_detection_reports?
  if Feature.enabled?(:show_child_security_reports_in_mr_widget, project)
    !!diff_head_pipeline&.complete_or_manual? &&
      pipeline_has_report_in_self_or_descendants?(:secret_detection)
  else
    !!diff_head_pipeline&.complete_or_manual_and_has_reports?(::Ci::JobArtifact.of_report_type(:secret_detection))
  end
end

#has_terraform_reports?Boolean

Returns:

  • (Boolean)


2124
2125
2126
# File 'app/models/merge_request.rb', line 2124

def has_terraform_reports?
  !!diff_head_pipeline&.complete_and_has_self_or_descendant_reports?(Ci::JobArtifact.of_report_type(:terraform))
end

#has_test_reports?Boolean

Returns:

  • (Boolean)


2078
2079
2080
# File 'app/models/merge_request.rb', line 2078

def has_test_reports?
  !!diff_head_pipeline&.has_test_reports?
end

#head_pipeline_active?Boolean

Returns:

  • (Boolean)


792
793
794
# File 'app/models/merge_request.rb', line 792

def head_pipeline_active?
  !!head_pipeline&.active?
end

#hidden?Boolean

Returns:

  • (Boolean)


2584
2585
2586
# File 'app/models/merge_request.rb', line 2584

def hidden?
  author&.banned?
end

#hook_attrsObject



929
930
931
# File 'app/models/merge_request.rb', line 929

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

#in_locked_stateObject



2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
# File 'app/models/merge_request.rb', line 2008

def in_locked_state
  # This method raises an error when adding to Redis set fails. This is so
  # we can retry merging if it wasn't added to ensure that the MR gets added
  # to locked set for unsticking in case it gets into a stuck state during
  # the merge process.
  add_to_locked_set
  lock_mr
  yield
ensure
  unlock_mr if locked?

  # We only remove it from the locked set if it's no longer locked as it means
  # the MR is either unlocked or merged.
  remove_from_locked_set unless locked?
end

#includes_ci_config?Boolean

Returns:

  • (Boolean)


2552
2553
2554
2555
2556
# File 'app/models/merge_request.rb', line 2552

def includes_ci_config?
  return false unless diff_stats

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

#invalidate_project_counter_cachesObject

rubocop: disable CodeReuse/ServiceClass



2452
2453
2454
# File 'app/models/merge_request.rb', line 2452

def invalidate_project_counter_caches
  Projects::OpenMergeRequestsCountService.new(target_project).delete_cache
end

#issues_mentioned_but_not_closing(current_user) ⇒ Object



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

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



2401
2402
2403
# File 'app/models/merge_request.rb', line 2401

def keep_around_commit
  project.repository.keep_around(self.merge_commit_sha, source: self.class.name)
end

#latest_diffs(diff_options = {}) ⇒ Object



1062
1063
1064
1065
1066
# File 'app/models/merge_request.rb', line 1062

def latest_diffs(diff_options = {})
  diff = diffable_merge_ref? ? merge_head_diff : merge_request_diff

  diff.diffs(diff_options)
end

#log_approval_deletion_on_merged_or_locked_mr(source:, current_user:, cause: nil) ⇒ Object



2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
# File 'app/models/merge_request.rb', line 2681

def log_approval_deletion_on_merged_or_locked_mr(source:, current_user:, cause: nil)
  return unless Feature.enabled?(:log_merged_mr_approval_deletion, target_project)
  return unless merged? || locked?

  Gitlab::AppLogger.warn(
    message: 'Approvals deleted on merged or locked MR',
    source: source,
    cause: cause,
    merge_request_id: id,
    merge_request_iid: iid,
    merge_request_state: state,
    project_id: target_project_id,
    current_user_id: current_user&.id
  )
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.



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

def merge_async(user_id, params)
  jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h)
  update_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



2433
2434
2435
2436
2437
2438
2439
# File 'app/models/merge_request.rb', line 2433

def merge_base_pipeline
  target_sha = diff_head_pipeline&.target_sha

  return unless target_sha

  target_branch_pipelines_for(sha: target_sha).last
end

#merge_blocked_by_other_mrs?Boolean

Returns:

  • (Boolean)


2568
2569
2570
# File 'app/models/merge_request.rb', line 2568

def merge_blocked_by_other_mrs?
  false # Overridden in EE
end

#merge_commitObject



2283
2284
2285
# File 'app/models/merge_request.rb', line 2283

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

#merge_eventObject



1468
1469
1470
# File 'app/models/merge_request.rb', line 1468

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

#merge_exclusive_leaseObject



2640
2641
2642
2643
2644
# File 'app/models/merge_request.rb', line 2640

def merge_exclusive_lease
  lease_key = ['merge_requests_merge_service', id].join(':')

  Gitlab::ExclusiveLease.new(lease_key, timeout: MERGE_LEASE_TIMEOUT)
end

#merge_ongoing?Boolean

Returns:

  • (Boolean)


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

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



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

def merge_participants
  participants = [author]

  if auto_merge_enabled? && participants.exclude?(merge_user)
    participants << merge_user
  end

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

#merge_pipelineObject



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

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

#merge_ref_headObject

Returns the current merge-ref HEAD commit.



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

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



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

def merge_ref_path
  "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge"
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.



115
116
117
118
119
# File 'app/models/merge_request.rb', line 115

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, order_by: :id_asc) ⇒ Object



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

def merge_request_diff_for(diff_refs_or_sha, order_by: :id_asc)
  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(order_by: order_by).find do |diff|
    diff.attributes.slice(*matcher.keys) == matcher
  end
end

#merge_request_reviewers_with(user_ids) ⇒ Object



2529
2530
2531
# File 'app/models/merge_request.rb', line 2529

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

#mergeability_checks_pass?(**params) ⇒ Boolean

This runs all the checks

Returns:

  • (Boolean)


1583
1584
1585
1586
1587
1588
1589
# File 'app/models/merge_request.rb', line 1583

def mergeability_checks_pass?(**params)
  execute_merge_checks(
    self.class.all_mergeability_checks,
    params: params,
    execute_all: false
  ).success?
end

#mergeable?(check_mergeability_retry_lease: false, skip_rebase_check: false, **mergeable_state_check_params) ⇒ Boolean

mergeable_state_check_params allows a hash of merge checks to skip or not skip_ci_check skip_discussions_check skip_draft_check skip_approved_check skip_blocked_check skip_external_status_check skip_requested_changes_check skip_locked_paths_check skip_jira_check skip_locked_lfs_files_check

Returns:

  • (Boolean)


1522
1523
1524
1525
1526
1527
# File 'app/models/merge_request.rb', line 1522

def mergeable?(check_mergeability_retry_lease: false, skip_rebase_check: false, **mergeable_state_check_params)
  return false unless mergeable_state?(**mergeable_state_check_params)

  check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
  mergeable_git_state?(skip_rebase_check: skip_rebase_check)
end

#mergeable_discussions_state?Boolean

Returns:

  • (Boolean)


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

def mergeable_discussions_state?
  return true unless only_allow_merge_if_all_discussions_are_resolved?

  unresolved_notes.none?(&:to_be_resolved?)
end

#mergeable_git_state?(**params) ⇒ Boolean

This runs only git related checks

Returns:

  • (Boolean)


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

def mergeable_git_state?(**params)
  execute_merge_checks(
    self.class.mergeable_git_state_checks,
    params: params,
    execute_all: false
  ).success?
end

#mergeable_state?(**params) ⇒ Boolean

mergeable_state_check_params allows a hash of merge checks to skip or not skip_ci_check skip_discussions_check skip_draft_check skip_approved_check skip_blocked_check skip_external_status_check skip_requested_changes_check skip_jira_check

Returns:

  • (Boolean)


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

def mergeable_state?(**params)
  execute_merge_checks(
    self.class.mergeable_state_checks,
    params: params,
    execute_all: false
  ).success?
end

#merged_atObject



2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
# File 'app/models/merge_request.rb', line 2342

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



2291
2292
2293
2294
2295
2296
# File 'app/models/merge_request.rb', line 2291

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)


2600
2601
2602
# File 'app/models/merge_request.rb', line 2600

def missing_required_squash?
  !squash && squash_always?
end

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



1129
1130
1131
1132
1133
1134
1135
1136
1137
# File 'app/models/merge_request.rb', line 1129

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



1144
1145
1146
# File 'app/models/merge_request.rb', line 1144

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

#non_latest_diffsObject



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

def non_latest_diffs
  merge_request_diffs.id_not_in(merge_request_diff.id)
end

#note_positions_for_paths(paths, user = nil) ⇒ Object



1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
# File 'app/models/merge_request.rb', line 1083

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)


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

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

#only_allow_merge_if_all_discussions_are_resolved?Boolean

Returns:

  • (Boolean)


1927
1928
1929
# File 'app/models/merge_request.rb', line 1927

def only_allow_merge_if_all_discussions_are_resolved?
  project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
end

#only_allow_merge_if_pipeline_succeeds?Boolean

Returns:

  • (Boolean)


1919
1920
1921
# File 'app/models/merge_request.rb', line 1919

def only_allow_merge_if_pipeline_succeeds?
  project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
end

#permits_force_push?Boolean

Returns:

  • (Boolean)


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

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



2413
2414
2415
2416
2417
# File 'app/models/merge_request.rb', line 2413

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

#pipeline_creating?Boolean

Returns:

  • (Boolean)


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

def pipeline_creating?
  Ci::PipelineCreation::Requests.pipeline_creating_for_merge_request?(self)
end

#predefined_variablesObject

rubocop: disable Metrics/AbcSize – Despite being long, this method is quite straightforward. Splitting it in smaller chunks would likely reduce readability.



2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
# File 'app/models/merge_request.rb', line 2083

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_DRAFT', value: work_in_progress?.to_s)

    mr_description, mr_description_truncated = truncate_mr_description
    variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION', value: mr_description)
    variables.append(key: 'CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED', value: mr_description_truncated)
    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

#preload_branchesObject



1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
# File 'app/models/merge_request.rb', line 1825

def preload_branches
  # There are cases when MergeRequest object is only partially loaded
  # Skip preloading if required fields are missing
  return unless has_attribute?(:source_project_id) && has_attribute?(:target_project_id)

  Gitlab::Git::RefPreloader.collect_ref(source_project_id, Gitlab::Git::BRANCH_REF_PREFIX + self.source_branch)
  Gitlab::Git::RefPreloader.collect_ref(target_project_id, Gitlab::Git::BRANCH_REF_PREFIX + self.target_branch)

  nil
end

#preload_commits_metadataObject



897
898
899
900
901
902
903
904
905
906
# File 'app/models/merge_request.rb', line 897

def 
  return unless merge_request_diff.persisted?

  ActiveRecord::Associations::Preloader.new(
    records: merge_request_diff.merge_request_diff_commits,
    associations: {
      merge_request_commits_metadata: [:commit_author, :committer]
    }
  ).call
end

#preloads_discussion_diff_highlighting?Boolean

Returns:

  • (Boolean)


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

def preloads_discussion_diff_highlighting?
  true
end

#prepared?Boolean

Returns:

  • (Boolean)


2592
2593
2594
# File 'app/models/merge_request.rb', line 2592

def prepared?
  prepared_at.present?
end

#previous_diffObject



1408
1409
1410
# File 'app/models/merge_request.rb', line 1408

def previous_diff
  merge_request_diffs.order(id: :desc).offset(1).take
end

#public_merge_statusObject

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



333
334
335
# File 'app/models/merge_request.rb', line 333

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

#raw_diffs(*args) ⇒ Object



1046
1047
1048
# File 'app/models/merge_request.rb', line 1046

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

#reached_diff_commits_limit?Boolean

Returns:

  • (Boolean)


2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
# File 'app/models/merge_request.rb', line 2703

def reached_diff_commits_limit?
  return false if Feature.disabled?(:merge_requests_diff_commits_limit, target_project)

  total_commits_count = MergeRequestDiff
    .from(merge_request_diffs.limit(1000), :limited_diffs)
    .pick('SUM(commits_count)')
    .to_i

  total_commits_count >= DIFF_COMMITS_LIMIT
end

#reached_versions_limit?Boolean

Returns:

  • (Boolean)


2697
2698
2699
2700
2701
# File 'app/models/merge_request.rb', line 2697

def reached_versions_limit?
  return false if Feature.disabled?(:merge_requests_diffs_limit, target_project)

  merge_request_diffs.count >= DIFF_VERSION_LIMIT
end

#real_time_notes_enabled?Boolean

Returns:

  • (Boolean)


2489
2490
2491
# File 'app/models/merge_request.rb', line 2489

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.



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

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)


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

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

#rebase_on_merge_pathObject



1969
1970
1971
# File 'app/models/merge_request.rb', line 1969

def rebase_on_merge_path
  "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/rebase_on_merge"
end

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



889
890
891
892
893
894
895
# File 'app/models/merge_request.rb', line 889

def recent_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: false, page: nil, preload_metadata: false)
  if  && Feature.enabled?(:merge_request_diff_commits_dedup, project) && !load_from_gitaly
    
  end

  commits(limit: limit, load_from_gitaly: load_from_gitaly, page: page)
end

#recent_context_commitsObject



944
945
946
# File 'app/models/merge_request.rb', line 944

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

#recent_diff_head_shas(limit = MAX_RECENT_DIFF_HEAD_SHAS) ⇒ Object



2257
2258
2259
2260
2261
2262
# File 'app/models/merge_request.rb', line 2257

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



2493
2494
2495
# File 'app/models/merge_request.rb', line 2493

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)


1464
1465
1466
# File 'app/models/merge_request.rb', line 1464

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

#ref_pathObject



1957
1958
1959
# File 'app/models/merge_request.rb', line 1957

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

#refs_to_cleanup(only: :all) ⇒ Object



1983
1984
1985
1986
1987
1988
1989
1990
# File 'app/models/merge_request.rb', line 1983

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 << rebase_on_merge_path if %i[all rebase_on_merge_path].include?(only)
  target_refs
end


1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
# File 'app/models/merge_request.rb', line 1781

def related_issues(user)
  visible_notes = user.can?(:read_internal_note, project) ? notes : notes.not_internal

  messages = [title, description, *visible_notes.pluck(:note)]
  messages += commits(load_from_gitaly: true).map(&:safe_message) if merge_request_diff.persisted?

  ext = Gitlab::ReferenceExtractor.new(project, user)
  ext.analyze(messages.join("\n"))

  ext.issues
end


1655
1656
1657
1658
1659
1660
1661
1662
1663
# File 'app/models/merge_request.rb', line 1655

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



1439
1440
1441
1442
1443
# File 'app/models/merge_request.rb', line 1439

def reload_diff(current_user = nil)
  return unless open?

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

#reload_diff_if_branch_changedObject



1431
1432
1433
1434
1435
1436
# File 'app/models/merge_request.rb', line 1431

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_from_locked_setObject



2664
2665
2666
2667
2668
# File 'app/models/merge_request.rb', line 2664

def remove_from_locked_set
  return unless self.class.use_locked_set?

  Gitlab::MergeRequests::LockedSet.remove(self.id)
end

#remove_source_branch?Boolean

Returns:

  • (Boolean)


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

def remove_source_branch?
  should_remove_source_branch? || force_remove_source_branch?
end

#reopenable?Boolean

Returns:

  • (Boolean)


1362
1363
1364
# File 'app/models/merge_request.rb', line 1362

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.



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

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



1973
1974
1975
1976
1977
1978
1979
1980
1981
# File 'app/models/merge_request.rb', line 1973

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_merged_commit_shaObject



2298
2299
2300
2301
2302
# File 'app/models/merge_request.rb', line 2298

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

#should_be_rebased?Boolean

Returns:

  • (Boolean)


1602
1603
1604
# File 'app/models/merge_request.rb', line 1602

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

#should_remove_source_branch?Boolean

Returns:

  • (Boolean)


1618
1619
1620
# File 'app/models/merge_request.rb', line 1618

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

#skipped_auto_merge_checks(options = {}) ⇒ Object



1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
# File 'app/models/merge_request.rb', line 1491

def skipped_auto_merge_checks(options = {})
  merge_when_checks_pass_strat = options[:auto_merge_strategy] == ::AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS || options[:auto_merge_strategy] == ::AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS

  {
    skip_ci_check: merge_when_checks_pass_strat,
    skip_approved_check: merge_when_checks_pass_strat,
    skip_draft_check: merge_when_checks_pass_strat,
    skip_blocked_check: merge_when_checks_pass_strat,
    skip_discussions_check: merge_when_checks_pass_strat,
    skip_external_status_check: merge_when_checks_pass_strat,
    skip_requested_changes_check: merge_when_checks_pass_strat,
    skip_locked_paths_check: merge_when_checks_pass_strat,
    skip_jira_check: merge_when_checks_pass_strat,
    skip_locked_lfs_files_check: merge_when_checks_pass_strat,
    skip_security_policy_check: merge_when_checks_pass_strat,
    skip_merge_time_check: merge_when_checks_pass_strat,
    skip_merge_request_title_check: merge_when_checks_pass_strat
  }
end

#source_and_target_branches_exist?Boolean

Returns:

  • (Boolean)


2646
2647
2648
# File 'app/models/merge_request.rb', line 2646

def source_and_target_branches_exist?
  source_branch_sha.present? && target_branch_sha.present?
end

#source_branch_exists?Boolean

Returns:

  • (Boolean)


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

def source_branch_exists?
  return false unless self.source_project

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

#source_branch_headObject



1216
1217
1218
1219
1220
1221
1222
# File 'app/models/merge_request.rb', line 1216

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_ref(or_sha: true) ⇒ Object



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

def source_branch_ref(or_sha: true)
  return @source_branch_sha if @source_branch_sha && or_sha
  return unless source_branch

  Gitlab::Git::BRANCH_REF_PREFIX + source_branch
end

#source_project_missing?Boolean

Returns:

  • (Boolean)


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

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



1809
1810
1811
1812
1813
1814
1815
# File 'app/models/merge_request.rb', line 1809

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

#source_project_pathObject



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

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

#squash_commitObject



2287
2288
2289
# File 'app/models/merge_request.rb', line 2287

def squash_commit
  @squash_commit ||= project.commit(squash_commit_sha) if squash_commit_sha
end

#squash_on_merge?Boolean

Returns:

  • (Boolean)


1893
1894
1895
1896
1897
1898
# File 'app/models/merge_request.rb', line 1893

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

  squash?
end

#squash_optionObject



2675
2676
2677
# File 'app/models/merge_request.rb', line 2675

def squash_option
  target_project.project_setting
end

#suggested_reviewer_usersObject

method overridden in EE



101
102
103
# File 'app/models/merge_request.rb', line 101

def suggested_reviewer_users
  User.none
end

#supports_assignee?Boolean

Returns:

  • (Boolean)


2517
2518
2519
# File 'app/models/merge_request.rb', line 2517

def supports_assignee?
  true
end

#supports_lock_on_merge?Boolean

Returns:

  • (Boolean)


992
993
994
995
996
# File 'app/models/merge_request.rb', line 992

def supports_lock_on_merge?
  return false unless merged?

  project.supports_lock_on_merge?
end

#supports_suggestion?Boolean

Returns:

  • (Boolean)


988
989
990
# File 'app/models/merge_request.rb', line 988

def supports_suggestion?
  true
end

#target_branch_exists?Boolean

Returns:

  • (Boolean)


1842
1843
1844
1845
1846
# File 'app/models/merge_request.rb', line 1842

def target_branch_exists?
  return false unless self.target_project

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

#target_branch_headObject



1224
1225
1226
1227
1228
# File 'app/models/merge_request.rb', line 1224

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

#target_branch_refObject



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

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)


2564
2565
2566
# File 'app/models/merge_request.rb', line 2564

def target_default_branch?
  target_branch == project.default_branch
end

#target_project_namespaceObject



1817
1818
1819
1820
1821
1822
1823
# File 'app/models/merge_request.rb', line 1817

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

#target_project_pathObject



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

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

#temporarily_unapproved?Boolean

Returns:

  • (Boolean)


2626
2627
2628
# File 'app/models/merge_request.rb', line 2626

def temporarily_unapproved?
  false
end

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

from argument can be a Namespace or Project.



934
935
936
937
938
# File 'app/models/merge_request.rb', line 934

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



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

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

#update_and_mark_in_progress_merge_commit_sha(commit_id) ⇒ Object



2024
2025
2026
2027
2028
2029
2030
# File 'app/models/merge_request.rb', line 2024

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



2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
# File 'app/models/merge_request.rb', line 2362

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



2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
# File 'app/models/merge_request.rb', line 2065

def update_head_pipeline
  find_diff_head_pipeline.try do |pipeline|
    self.head_pipeline = pipeline

    next unless head_pipeline_id_changed?

    update_columns(
      head_pipeline_id: head_pipeline.id,
      retargeted: false
    )
  end
end

#update_merge_jid(jid) ⇒ Object



2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
# File 'app/models/merge_request.rb', line 2796

def update_merge_jid(jid)
  with_single_transaction_merge_data_sync(
    -> { update_column(:merge_jid, jid) },
    -> do
      # update_column only works if the record is already saved.
      if merge_data.persisted?
        merge_data.update_column(:merge_jid, jid)
      else
        merge_data.update_attribute(:merge_jid, jid)
      end
    end
  )
end

#update_merge_ref_sha(sha) ⇒ Object



2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
# File 'app/models/merge_request.rb', line 2782

def update_merge_ref_sha(sha)
  with_single_transaction_merge_data_sync(
    -> { update_column(:merge_ref_sha, sha) },
    -> do
      # update_column only works if the record is already saved.
      if merge_data.persisted?
        merge_data.update_column(:merge_ref_sha, sha)
      else
        merge_data.update_attribute(:merge_ref_sha, sha)
      end
    end
  )
end

#validate_branch_name(attr) ⇒ Object



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

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



1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
# File 'app/models/merge_request.rb', line 1283

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



1328
1329
1330
1331
1332
1333
1334
# File 'app/models/merge_request.rb', line 1328

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



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

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



1322
1323
1324
1325
1326
# File 'app/models/merge_request.rb', line 1322

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



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

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

#visible_closing_issues_for(current_user = self.author) ⇒ Object



1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
# File 'app/models/merge_request.rb', line 1738

def visible_closing_issues_for(current_user = self.author)
  strong_memoize_with(:visible_closing_issues_for, current_user&.id) do
    visible_issues = 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

    ActiveRecord::Associations::Preloader.new(
      records: visible_issues.select { |issue| issue.is_a?(Issue) },
      associations: :project
    ).call
    # Exclude issues that have been cached but their project setting has been disabled, or they belong to a group
    visible_issues.select do |issue|
      !issue.is_a?(Issue) || issue.autoclose_by_merged_closing_merge_request?
    end
  end
end