1# frozen_string_literal: true
2
3class MergeRequest < ApplicationRecord
4  include AtomicInternalId
5  include IidRoutes
6  include Issuable
7  include Noteable
8  include Referable
9  include Presentable
10  include TimeTrackable
11  include ManualInverseAssociation
12  include EachBatch
13  include ThrottledTouch
14  include Gitlab::Utils::StrongMemoize
15  include LabelEventable
16  include ReactiveCaching
17  include FromUnion
18  include DeprecatedAssignee
19  include ShaAttribute
20  include IgnorableColumns
21  include MilestoneEventable
22  include StateEventable
23  include ApprovableBase
24  include IdInOrdered
25  include Todoable
26
27  extend ::Gitlab::Utils::Override
28
29  sha_attribute :squash_commit_sha
30  sha_attribute :merge_ref_sha
31
32  self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
33  self.reactive_cache_refresh_interval = 10.minutes
34  self.reactive_cache_lifetime = 10.minutes
35  self.reactive_cache_work_type = :no_dependency
36
37  SORTING_PREFERENCE_FIELD = :merge_requests_sort
38
39  ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
40    'Ci::CompareMetricsReportsService'     => ->(project) { true },
41    'Ci::CompareCodequalityReportsService' => ->(project) { true }
42  }.freeze
43
44  belongs_to :target_project, class_name: "Project"
45  belongs_to :source_project, class_name: "Project"
46  belongs_to :merge_user, class_name: "User"
47  belongs_to :iteration, foreign_key: 'sprint_id'
48
49  has_internal_id :iid, scope: :target_project, track_if: -> { !importing? },
50    init: ->(mr, scope) do
51      if mr
52        mr.target_project&.merge_requests&.maximum(:iid)
53      elsif scope[:project]
54        where(target_project: scope[:project]).maximum(:iid)
55      end
56    end
57
58  has_many :merge_request_diffs,
59    -> { regular }, inverse_of: :merge_request
60  has_many :merge_request_context_commits, inverse_of: :merge_request
61  has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files
62
63  has_one :merge_request_diff,
64    -> { regular.order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
65  has_one :merge_head_diff,
66    -> { merge_head }, inverse_of: :merge_request, class_name: 'MergeRequestDiff'
67  has_one :cleanup_schedule, inverse_of: :merge_request
68
69  belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
70  manual_inverse_association :latest_merge_request_diff, :merge_request
71
72  # This is the same as latest_merge_request_diff unless:
73  # 1. There are arguments - in which case we might be trying to force-reload.
74  # 2. This association is already loaded.
75  # 3. The latest diff does not exist.
76  # 4. It doesn't have any merge_request_diffs - it returns an empty MergeRequestDiff
77  #
78  # The second one in particular is important - MergeRequestDiff#merge_request
79  # is the inverse of MergeRequest#merge_request_diff, which means it may not be
80  # the latest diff, because we could have loaded any diff from this particular
81  # MR. If we haven't already loaded a diff, then it's fine to load the latest.
82  def merge_request_diff
83    fallback = latest_merge_request_diff unless association(:merge_request_diff).loaded?
84
85    fallback || super || MergeRequestDiff.new(merge_request_id: id)
86  end
87
88  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
89
90  has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
91
92  has_many :merge_requests_closing_issues,
93    class_name: 'MergeRequestsClosingIssues',
94    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
95
96  has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
97  has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
98  has_many :suggestions, through: :notes
99  has_many :unresolved_notes, -> { unresolved }, as: :noteable, class_name: 'Note'
100
101  has_many :merge_request_assignees
102  has_many :assignees, class_name: "User", through: :merge_request_assignees
103  has_many :merge_request_reviewers
104  has_many :reviewers, class_name: "User", through: :merge_request_reviewers
105  has_many :user_mentions, class_name: "MergeRequestUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
106
107  has_many :deployment_merge_requests
108
109  # These are deployments created after the merge request has been merged, and
110  # the merge request was tracked explicitly (instead of implicitly using a CI
111  # build).
112  has_many :deployments,
113    through: :deployment_merge_requests
114
115  has_many :draft_notes
116  has_many :reviews, inverse_of: :merge_request
117
118  KNOWN_MERGE_PARAMS = [
119    :auto_merge_strategy,
120    :should_remove_source_branch,
121    :force_remove_source_branch,
122    :commit_message,
123    :squash_commit_message,
124    :sha
125  ].freeze
126  serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
127
128  before_validation :set_draft_status
129
130  after_create :ensure_merge_request_diff
131  after_update :clear_memoized_shas
132  after_update :reload_diff_if_branch_changed
133  after_commit :ensure_metrics, on: [:create, :update], unless: :importing?
134  after_commit :expire_etag_cache, unless: :importing?
135
136  # When this attribute is true some MR validation is ignored
137  # It allows us to close or modify broken merge requests
138  attr_accessor :allow_broken
139
140  # Temporary fields to store compare vars
141  # when creating new merge request
142  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
143
144  participant :reviewers
145
146  # Keep states definition to be evaluated before the state_machine block to avoid spec failures.
147  # If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil.
148  def self.available_state_names
149    super + [:merged, :locked]
150  end
151
152  state_machine :state_id, initial: :opened, initialize: false do
153    event :close do
154      transition [:opened] => :closed
155    end
156
157    event :mark_as_merged do
158      transition [:opened, :locked] => :merged
159    end
160
161    event :reopen do
162      transition closed: :opened
163    end
164
165    event :lock_mr do
166      transition [:opened] => :locked
167    end
168
169    event :unlock_mr do
170      transition locked: :opened
171    end
172
173    before_transition any => :opened do |merge_request|
174      merge_request.merge_jid = nil
175    end
176
177    after_transition any => :opened do |merge_request|
178      merge_request.run_after_commit do
179        UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
180      end
181    end
182
183    state :opened, value: MergeRequest.available_states[:opened]
184    state :closed, value: MergeRequest.available_states[:closed]
185    state :merged, value: MergeRequest.available_states[:merged]
186    state :locked, value: MergeRequest.available_states[:locked]
187  end
188
189  # Alias to state machine .with_state_id method
190  # This needs to be defined after the state machine block to avoid errors
191  class << self
192    alias_method :with_state, :with_state_id
193    alias_method :with_states, :with_state_ids
194  end
195
196  state_machine :merge_status, initial: :unchecked do
197    event :mark_as_preparing do
198      transition unchecked: :preparing
199    end
200
201    event :mark_as_unchecked do
202      transition [:preparing, :can_be_merged, :checking] => :unchecked
203      transition [:cannot_be_merged, :cannot_be_merged_rechecking] => :cannot_be_merged_recheck
204    end
205
206    event :mark_as_checking do
207      transition unchecked: :checking
208      transition cannot_be_merged_recheck: :cannot_be_merged_rechecking
209    end
210
211    event :mark_as_mergeable do
212      transition [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking] => :can_be_merged
213    end
214
215    event :mark_as_unmergeable do
216      transition [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking] => :cannot_be_merged
217    end
218
219    state :preparing
220    state :unchecked
221    state :cannot_be_merged_recheck
222    state :checking
223    state :cannot_be_merged_rechecking
224    state :can_be_merged
225    state :cannot_be_merged
226
227    around_transition do |merge_request, transition, block|
228      Gitlab::Timeless.timeless(merge_request, &block)
229    end
230
231    # rubocop: disable CodeReuse/ServiceClass
232    after_transition [:unchecked, :checking] => :cannot_be_merged do |merge_request, transition|
233      if merge_request.notify_conflict?
234        NotificationService.new.merge_request_unmergeable(merge_request)
235        TodoService.new.merge_request_became_unmergeable(merge_request)
236      end
237    end
238    # rubocop: enable CodeReuse/ServiceClass
239
240    def check_state?(merge_status)
241      [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking].include?(merge_status.to_sym)
242    end
243  end
244
245  # Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking`
246  # to avoid exposing unnecessary internal state
247  def public_merge_status
248    cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status
249  end
250
251  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
252  validates :source_branch, presence: true
253  validates :target_project, presence: true
254  validates :target_branch, presence: true
255  validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing?
256  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
257  validate :validate_fork, unless: :closed_or_merged_without_fork?
258  validate :validate_target_project, on: :create
259
260  scope :by_source_or_target_branch, ->(branch_name) do
261    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
262  end
263  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
264  scope :of_projects, ->(ids) { where(target_project_id: ids) }
265  scope :from_project, ->(project) { where(source_project_id: project.id) }
266  scope :from_fork, -> { where('source_project_id <> target_project_id') }
267  scope :from_and_to_forks, ->(project) do
268    from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
269  end
270  scope :merged, -> { with_state(:merged) }
271  scope :open_and_closed, -> { with_states(:opened, :closed) }
272  scope :drafts, -> { where(draft: true) }
273  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
274  scope :by_commit_sha, ->(sha) do
275    where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
276  end
277  scope :by_merge_commit_sha, -> (sha) do
278    where(merge_commit_sha: sha)
279  end
280  scope :by_squash_commit_sha, -> (sha) do
281    where(squash_commit_sha: sha)
282  end
283  scope :by_merge_or_squash_commit_sha, -> (sha) do
284    from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha)])
285  end
286  scope :by_related_commit_sha, -> (sha) do
287    from_union(
288      [
289        by_commit_sha(sha),
290        by_squash_commit_sha(sha),
291        by_merge_commit_sha(sha)
292      ]
293    )
294  end
295  scope :join_project, -> { joins(:target_project) }
296  scope :join_metrics, -> (target_project_id = nil) do
297    # Do not join the relation twice
298    return self if self.arel.join_sources.any? { |join| join.left.try(:name).eql?(MergeRequest::Metrics.table_name) }
299
300    query = joins(:metrics)
301
302    if !target_project_id && self.where_values_hash["target_project_id"]
303      target_project_id = self.where_values_hash["target_project_id"]
304      query = query.unscope(where: :target_project_id)
305    end
306
307    project_condition = if target_project_id
308                          MergeRequest::Metrics.arel_table[:target_project_id].eq(target_project_id)
309                        else
310                          MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id])
311                        end
312
313    query.where(project_condition)
314  end
315  scope :references_project, -> { references(:target_project) }
316  scope :with_api_entity_associations, -> {
317    preload_routables
318      .preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
319               :timelogs, :latest_merge_request_diff, :reviewers,
320               target_project: :project_feature,
321               metrics: [:latest_closed_by, :merged_by])
322  }
323
324  scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
325  scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
326
327  scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
328    where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
329  end
330  scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
331  scope :order_by_metric, ->(metric, direction) do
332    reverse_direction = { 'ASC' => 'DESC', 'DESC' => 'ASC' }
333    reversed_direction = reverse_direction[direction] || raise("Unknown sort direction was given: #{direction}")
334
335    order = Gitlab::Pagination::Keyset::Order.build([
336      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
337        attribute_name: "merge_request_metrics_#{metric}",
338        column_expression: MergeRequest::Metrics.arel_table[metric],
339        order_expression: Gitlab::Database.nulls_last_order("merge_request_metrics.#{metric}", direction),
340        reversed_order_expression: Gitlab::Database.nulls_first_order("merge_request_metrics.#{metric}", reversed_direction),
341        order_direction: direction,
342        nullable: :nulls_last,
343        distinct: false,
344        add_to_projections: true
345      ),
346      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
347        attribute_name: 'merge_request_metrics_id',
348        order_expression: MergeRequest::Metrics.arel_table[:id].desc,
349        add_to_projections: true
350      )
351    ])
352
353    order.apply_cursor_conditions(join_metrics).order(order)
354  end
355  scope :order_merged_at_asc, -> { order_by_metric(:merged_at, 'ASC') }
356  scope :order_merged_at_desc, -> { order_by_metric(:merged_at, 'DESC') }
357  scope :order_closed_at_asc, -> { order_by_metric(:latest_closed_at, 'ASC') }
358  scope :order_closed_at_desc, -> { order_by_metric(:latest_closed_at, 'DESC') }
359  scope :preload_source_project, -> { preload(:source_project) }
360  scope :preload_target_project, -> { preload(:target_project) }
361  scope :preload_routables, -> do
362    preload(target_project: [:route, { namespace: :route }],
363            source_project: [:route, { namespace: :route }])
364  end
365  scope :preload_author, -> { preload(:author) }
366  scope :preload_approved_by_users, -> { preload(:approved_by_users) }
367  scope :preload_metrics, -> (relation) { preload(metrics: relation) }
368  scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) }
369  scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: { merge_request_diff_commits: [:commit_author, :committer] }) }
370  scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) }
371
372  scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) }
373
374  scope :with_auto_merge_enabled, -> do
375    with_state(:opened).where(auto_merge_enabled: true)
376  end
377
378  scope :including_metrics, -> do
379    includes(:metrics)
380  end
381
382  scope :with_jira_issue_keys, -> { where('title ~ :regex OR merge_requests.description ~ :regex', regex: Gitlab::Regex.jira_issue_key_regex.source) }
383
384  scope :review_requested, -> do
385    where(reviewers_subquery.exists)
386  end
387
388  scope :no_review_requested, -> do
389    where(reviewers_subquery.exists.not)
390  end
391
392  scope :review_requested_to, ->(user) do
393    where(
394      reviewers_subquery
395        .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id))
396        .exists
397    )
398  end
399
400  scope :no_review_requested_to, ->(user) do
401    where(
402      reviewers_subquery
403        .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id))
404        .exists
405        .not
406    )
407  end
408
409  def self.total_time_to_merge
410    join_metrics
411      .merge(MergeRequest::Metrics.with_valid_time_to_merge)
412      .pluck(MergeRequest::Metrics.time_to_merge_expression)
413      .first
414  end
415
416  after_save :keep_around_commit, unless: :importing?
417
418  alias_attribute :project, :target_project
419  alias_attribute :project_id, :target_project_id
420
421  # Currently, `merge_when_pipeline_succeeds` column is used as a flag
422  # to check if _any_ auto merge strategy is activated on the merge request.
423  # Today, we have multiple strategies and MWPS is one of them.
424  # we'd eventually rename the column for avoiding confusions, but in the mean time
425  # please use `auto_merge_enabled` alias instead of `merge_when_pipeline_succeeds`.
426  alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
427  alias_method :issuing_parent, :target_project
428
429  delegate :builds_with_coverage, to: :head_pipeline, prefix: true, allow_nil: true
430
431  RebaseLockTimeout = Class.new(StandardError)
432
433  def self.reference_prefix
434    '!'
435  end
436
437  # Returns the top 100 target branches
438  #
439  # The returned value is a Array containing branch names
440  # sort by updated_at of merge request:
441  #
442  #     ['master', 'develop', 'production']
443  #
444  # limit - The maximum number of target branch to return.
445  def self.recent_target_branches(limit: 100)
446    group(:target_branch)
447      .select(:target_branch)
448      .reorder(arel_table[:updated_at].maximum.desc)
449      .limit(limit)
450      .pluck(:target_branch)
451  end
452
453  def self.sort_by_attribute(method, excluded_labels: [])
454    case method.to_s
455    when 'merged_at', 'merged_at_asc' then order_merged_at_asc
456    when 'closed_at', 'closed_at_asc' then order_closed_at_asc
457    when 'merged_at_desc' then order_merged_at_desc
458    when 'closed_at_desc' then order_closed_at_desc
459    else
460      super
461    end
462  end
463
464  def self.reviewers_subquery
465    MergeRequestReviewer.arel_table
466      .project('true')
467      .where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
468  end
469
470  def rebase_in_progress?
471    rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
472  end
473
474  # Use this method whenever you need to make sure the head_pipeline is synced with the
475  # branch head commit, for example checking if a merge request can be merged.
476  # For more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/40004
477  def actual_head_pipeline
478    head_pipeline&.matches_sha_or_source_sha?(diff_head_sha) ? head_pipeline : nil
479  end
480
481  def merge_pipeline
482    return unless merged?
483
484    # When the merge_method is :merge there will be a merge_commit_sha, however
485    # when it is fast-forward there is no merge commit, so we must fall back to
486    # either the squash commit (if the MR was squashed) or the diff head commit.
487    sha = merge_commit_sha || squash_commit_sha || diff_head_sha
488    target_project.latest_pipeline(target_branch, sha)
489  end
490
491  def head_pipeline_active?
492    !!head_pipeline&.active?
493  end
494
495  def actual_head_pipeline_active?
496    !!actual_head_pipeline&.active?
497  end
498
499  def actual_head_pipeline_success?
500    !!actual_head_pipeline&.success?
501  end
502
503  # Pattern used to extract `!123` merge request references from text
504  #
505  # This pattern supports cross-project references.
506  def self.reference_pattern
507    @reference_pattern ||= %r{
508      (#{Project.reference_pattern})?
509      #{Regexp.escape(reference_prefix)}#{Gitlab::Regex.merge_request}
510    }x
511  end
512
513  def self.link_reference_pattern
514    @link_reference_pattern ||= super("merge_requests", Gitlab::Regex.merge_request)
515  end
516
517  def self.reference_valid?(reference)
518    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
519  end
520
521  def self.project_foreign_key
522    'target_project_id'
523  end
524
525  # Returns all the merge requests from an ActiveRecord:Relation.
526  #
527  # This method uses a UNION as it usually operates on the result of
528  # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
529  # using multiple sub-queries especially when combined with an OR statement.
530  # UNIONs on the other hand perform much better in these cases.
531  #
532  # relation - An ActiveRecord::Relation that returns a list of Projects.
533  #
534  # Returns an ActiveRecord::Relation.
535  def self.in_projects(relation)
536    # unscoping unnecessary conditions that'll be applied
537    # when executing `where("merge_requests.id IN (#{union.to_sql})")`
538    source = unscoped.where(source_project_id: relation)
539    target = unscoped.where(target_project_id: relation)
540
541    from_union([source, target])
542  end
543
544  # This is used after project import, to reset the IDs to the correct
545  # values. It is not intended to be called without having already scoped the
546  # relation.
547  #
548  # Only set `regular` merge request diffs as latest so `merge_head` diff
549  # won't be considered as `MergeRequest#merge_request_diff`.
550  def self.set_latest_merge_request_diff_ids!
551    update = "
552      latest_merge_request_diff_id = (
553        SELECT MAX(id)
554        FROM merge_request_diffs
555        WHERE merge_requests.id = merge_request_diffs.merge_request_id
556        AND merge_request_diffs.diff_type = #{MergeRequestDiff.diff_types[:regular]}
557      )".squish
558
559    self.each_batch do |batch|
560      batch.update_all(update)
561    end
562  end
563
564  # WIP is deprecated in favor of Draft. Currently both options are supported
565  # https://gitlab.com/gitlab-org/gitlab/-/issues/227426
566  DRAFT_REGEX = /\A*#{Regexp.union(Gitlab::Regex.merge_request_wip, Gitlab::Regex.merge_request_draft)}+\s*/i.freeze
567
568  def self.work_in_progress?(title)
569    !!(title =~ DRAFT_REGEX)
570  end
571
572  def self.wipless_title(title)
573    title.sub(DRAFT_REGEX, "")
574  end
575
576  def self.wip_title(title)
577    work_in_progress?(title) ? title : "Draft: #{title}"
578  end
579
580  def self.participant_includes
581    [:reviewers, :award_emoji] + super
582  end
583
584  def committers
585    @committers ||= commits.committers
586  end
587
588  # Verifies if title has changed not taking into account Draft prefix
589  # for merge requests.
590  def wipless_title_changed(old_title)
591    self.class.wipless_title(old_title) != self.wipless_title
592  end
593
594  def hook_attrs
595    Gitlab::HookData::MergeRequestBuilder.new(self).build
596  end
597
598  # `from` argument can be a Namespace or Project.
599  def to_reference(from = nil, full: false)
600    reference = "#{self.class.reference_prefix}#{iid}"
601
602    "#{project.to_reference_base(from, full: full)}#{reference}"
603  end
604
605  def context_commits(limit: nil)
606    @context_commits ||= merge_request_context_commits.order_by_committed_date_desc.limit(limit).map(&:to_commit)
607  end
608
609  def recent_context_commits
610    context_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE)
611  end
612
613  def context_commits_count
614    context_commits.count
615  end
616
617  def commits(limit: nil, load_from_gitaly: false)
618    return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly) if merge_request_diff.persisted?
619
620    commits_arr = if compare_commits
621                    reversed_commits = compare_commits.reverse
622                    limit ? reversed_commits.take(limit) : reversed_commits
623                  else
624                    []
625                  end
626
627    CommitCollection.new(source_project, commits_arr, source_branch)
628  end
629
630  def recent_commits(load_from_gitaly: false)
631    commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: load_from_gitaly)
632  end
633
634  def commits_count
635    if merge_request_diff.persisted?
636      merge_request_diff.commits_count
637    elsif compare_commits
638      compare_commits.size
639    else
640      0
641    end
642  end
643
644  def commit_shas(limit: nil)
645    return merge_request_diff.commit_shas(limit: limit) if merge_request_diff.persisted?
646
647    shas =
648      if compare_commits
649        compare_commits.to_a.reverse.map(&:sha)
650      else
651        Array(diff_head_sha)
652      end
653
654    limit ? shas.take(limit) : shas
655  end
656
657  def supports_suggestion?
658    true
659  end
660
661  # Calls `MergeWorker` to proceed with the merge process and
662  # updates `merge_jid` with the MergeWorker#jid.
663  # This helps tracking enqueued and ongoing merge jobs.
664  def merge_async(user_id, params)
665    jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h)
666    update_column(:merge_jid, jid)
667
668    # merge_ongoing? depends on merge_jid
669    # expire etag cache since the attribute is changed without triggering callbacks
670    expire_etag_cache
671  end
672
673  # Set off a rebase asynchronously, atomically updating the `rebase_jid` of
674  # the MR so that the status of the operation can be tracked.
675  def rebase_async(user_id, skip_ci: false)
676    with_rebase_lock do
677      raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress?
678
679      # Although there is a race between setting rebase_jid here and clearing it
680      # in the RebaseWorker, it can't do any harm since we check both that the
681      # attribute is set *and* that the sidekiq job is still running. So a JID
682      # for a completed RebaseWorker is equivalent to a nil JID.
683      jid = Sidekiq::Worker.skipping_transaction_check do
684        RebaseWorker.with_status.perform_async(id, user_id, skip_ci)
685      end
686
687      update_column(:rebase_jid, jid)
688    end
689
690    # rebase_in_progress? depends on rebase_jid
691    # expire etag cache since the attribute is changed without triggering callbacks
692    expire_etag_cache
693  end
694
695  def merge_participants
696    participants = [author]
697
698    if auto_merge_enabled? && !participants.include?(merge_user)
699      participants << merge_user
700    end
701
702    participants.select { |participant| Ability.allowed?(participant, :read_merge_request, self) }
703  end
704
705  def first_commit
706    compare_commits.present? ? compare_commits.first : merge_request_diff.first_commit
707  end
708
709  def raw_diffs(*args)
710    compare.present? ? compare.raw_diffs(*args) : merge_request_diff.raw_diffs(*args)
711  end
712
713  def diffs(diff_options = {})
714    if compare
715      # When saving MR diffs, `expanded` is implicitly added (because we need
716      # to save the entire contents to the DB), so add that here for
717      # consistency.
718      compare.diffs(diff_options.merge(expanded: true))
719    else
720      merge_request_diff.diffs(diff_options)
721    end
722  end
723
724  def non_latest_diffs
725    merge_request_diffs.where.not(id: merge_request_diff.id)
726  end
727
728  def note_positions_for_paths(paths, user = nil)
729    positions = notes.new_diff_notes.joins(:note_diff_file)
730      .where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths)
731      .positions
732
733    collection = Gitlab::Diff::PositionCollection.new(positions, diff_head_sha)
734
735    return collection unless user
736
737    positions = draft_notes
738      .authored_by(user)
739      .positions
740      .select { |pos| paths.include?(pos.file_path) }
741
742    collection.concat(positions)
743  end
744
745  def preloads_discussion_diff_highlighting?
746    true
747  end
748
749  def discussions_diffs
750    strong_memoize(:discussions_diffs) do
751      note_diff_files = NoteDiffFile
752        .joins(:diff_note)
753        .merge(notes.or(commit_notes))
754        .includes(diff_note: :project)
755
756      Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
757    end
758  end
759
760  def diff_stats
761    return unless diff_refs
762
763    strong_memoize(:diff_stats) do
764      project.repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha)
765    end
766  end
767
768  def diff_size
769    # Calling `merge_request_diff.diffs.real_size` will also perform
770    # highlighting, which we don't need here.
771    merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size
772  end
773
774  def modified_paths(past_merge_request_diff: nil, fallback_on_overflow: false)
775    if past_merge_request_diff
776      past_merge_request_diff.modified_paths(fallback_on_overflow: fallback_on_overflow)
777    elsif compare
778      diff_stats&.paths || compare.modified_paths
779    else
780      merge_request_diff.modified_paths(fallback_on_overflow: fallback_on_overflow)
781    end
782  end
783
784  def new_paths
785    diffs.diff_files.map(&:new_path)
786  end
787
788  def diff_base_commit
789    if merge_request_diff.persisted?
790      merge_request_diff.base_commit
791    else
792      branch_merge_base_commit
793    end
794  end
795
796  def diff_start_commit
797    if merge_request_diff.persisted?
798      merge_request_diff.start_commit
799    else
800      target_branch_head
801    end
802  end
803
804  def diff_head_commit
805    if merge_request_diff.persisted?
806      merge_request_diff.head_commit
807    else
808      source_branch_head
809    end
810  end
811
812  def diff_start_sha
813    if merge_request_diff.persisted?
814      merge_request_diff.start_commit_sha
815    else
816      target_branch_head.try(:sha)
817    end
818  end
819
820  def diff_base_sha
821    if merge_request_diff.persisted?
822      merge_request_diff.base_commit_sha
823    else
824      branch_merge_base_commit.try(:sha)
825    end
826  end
827
828  def diff_head_sha
829    if merge_request_diff.persisted?
830      merge_request_diff.head_commit_sha
831    else
832      source_branch_head.try(:sha)
833    end
834  end
835
836  # When importing a pull request from GitHub, the old and new branches may no
837  # longer actually exist by those names, but we need to recreate the merge
838  # request diff with the right source and target shas.
839  # We use these attributes to force these to the intended values.
840  attr_writer :target_branch_sha, :source_branch_sha
841
842  def source_branch_ref
843    return @source_branch_sha if @source_branch_sha
844    return unless source_branch
845
846    Gitlab::Git::BRANCH_REF_PREFIX + source_branch
847  end
848
849  def target_branch_ref
850    return @target_branch_sha if @target_branch_sha
851    return unless target_branch
852
853    Gitlab::Git::BRANCH_REF_PREFIX + target_branch
854  end
855
856  def source_branch_head
857    strong_memoize(:source_branch_head) do
858      if source_project && source_branch_ref
859        source_project.repository.commit(source_branch_ref)
860      end
861    end
862  end
863
864  def target_branch_head
865    strong_memoize(:target_branch_head) do
866      target_project.repository.commit(target_branch_ref)
867    end
868  end
869
870  def branch_merge_base_commit
871    start_sha = target_branch_sha
872    head_sha  = source_branch_sha
873
874    if start_sha && head_sha
875      target_project.merge_base_commit(start_sha, head_sha)
876    end
877  end
878
879  def target_branch_sha
880    @target_branch_sha || target_branch_head.try(:sha)
881  end
882
883  def source_branch_sha
884    @source_branch_sha || source_branch_head.try(:sha)
885  end
886
887  def diff_refs
888    if importing? || persisted?
889      merge_request_diff.diff_refs
890    else
891      repository_diff_refs
892    end
893  end
894
895  # Instead trying to fetch the
896  # persisted diff_refs, this method goes
897  # straight to the repository to get the
898  # most recent data possible.
899  def repository_diff_refs
900    Gitlab::Diff::DiffRefs.new(
901      base_sha:  branch_merge_base_sha,
902      start_sha: target_branch_sha,
903      head_sha:  source_branch_sha
904    )
905  end
906
907  def branch_merge_base_sha
908    branch_merge_base_commit.try(:sha)
909  end
910
911  def validate_branches
912    return unless target_project && source_project
913
914    if target_project == source_project && target_branch == source_branch
915      errors.add :branch_conflict, "You can't use same project/branch for source and target"
916      return
917    end
918
919    [:source_branch, :target_branch].each { |attr| validate_branch_name(attr) }
920
921    if opened?
922      similar_mrs = target_project
923        .merge_requests
924        .where(source_branch: source_branch, target_branch: target_branch)
925        .where(source_project_id: source_project&.id)
926        .opened
927
928      similar_mrs = similar_mrs.where.not(id: id) if persisted?
929
930      conflict = similar_mrs.first
931
932      if conflict.present?
933        errors.add(
934          :validate_branches,
935          "Another open merge request already exists for this source branch: #{conflict.to_reference}"
936        )
937      end
938    end
939  end
940
941  def validate_branch_name(attr)
942    return unless will_save_change_to_attribute?(attr)
943
944    branch = read_attribute(attr)
945
946    return unless branch
947
948    errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch)
949  end
950
951  def validate_target_project
952    return true if target_project.merge_requests_enabled?
953
954    errors.add :base, 'Target project has disabled merge requests'
955  end
956
957  def validate_fork
958    return true unless target_project && source_project
959    return true if target_project == source_project
960    return true unless source_project_missing?
961
962    errors.add :validate_fork,
963               'Source project is not a fork of the target project'
964  end
965
966  def merge_ongoing?
967    # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
968    # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
969    return true if locked?
970
971    !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
972  end
973
974  def closed_or_merged_without_fork?
975    (closed? || merged?) && source_project_missing?
976  end
977
978  def source_project_missing?
979    return false unless for_fork?
980    return true unless source_project
981
982    !source_project.in_fork_network_of?(target_project)
983  end
984
985  def reopenable?
986    closed? && !source_project_missing? && source_branch_exists?
987  end
988
989  def can_be_closed?
990    opened?
991  end
992
993  def ensure_merge_request_diff
994    merge_request_diff.persisted? || create_merge_request_diff
995  end
996
997  def create_merge_request_diff
998    fetch_ref!
999
1000    # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377
1001    Gitlab::GitalyClient.allow_n_plus_1_calls do
1002      merge_request_diffs.create!
1003      reload_merge_request_diff
1004    end
1005  end
1006
1007  def viewable_diffs
1008    @viewable_diffs ||= merge_request_diffs.viewable.to_a
1009  end
1010
1011  def merge_request_diff_for(diff_refs_or_sha)
1012    matcher =
1013      if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
1014        {
1015          'start_commit_sha' => diff_refs_or_sha.start_sha,
1016          'head_commit_sha' => diff_refs_or_sha.head_sha,
1017          'base_commit_sha' => diff_refs_or_sha.base_sha
1018        }
1019      else
1020        { 'head_commit_sha' => diff_refs_or_sha }
1021      end
1022
1023    viewable_diffs.find do |diff|
1024      diff.attributes.slice(*matcher.keys) == matcher
1025    end
1026  end
1027
1028  def version_params_for(diff_refs)
1029    if diff = merge_request_diff_for(diff_refs)
1030      { diff_id: diff.id }
1031    elsif diff = merge_request_diff_for(diff_refs.head_sha)
1032      {
1033        diff_id: diff.id,
1034        start_sha: diff_refs.start_sha
1035      }
1036    end
1037  end
1038
1039  def clear_memoized_shas
1040    @target_branch_sha = @source_branch_sha = nil
1041
1042    clear_memoization(:source_branch_head)
1043    clear_memoization(:target_branch_head)
1044  end
1045
1046  def reload_diff_if_branch_changed
1047    if (saved_change_to_source_branch? || saved_change_to_target_branch?) &&
1048        (source_branch_head && target_branch_head)
1049      reload_diff
1050    end
1051  end
1052
1053  # rubocop: disable CodeReuse/ServiceClass
1054  def reload_diff(current_user = nil)
1055    return unless open?
1056
1057    MergeRequests::ReloadDiffsService.new(self, current_user).execute
1058  end
1059
1060  def check_mergeability(async: false)
1061    return unless recheck_merge_status?
1062
1063    check_service = MergeRequests::MergeabilityCheckService.new(self)
1064
1065    if async
1066      check_service.async_execute
1067    else
1068      check_service.execute(retry_lease: false)
1069    end
1070  end
1071  # rubocop: enable CodeReuse/ServiceClass
1072
1073  def diffable_merge_ref?
1074    open? && merge_head_diff.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?)
1075  end
1076
1077  # Returns boolean indicating the merge_status should be rechecked in order to
1078  # switch to either can_be_merged or cannot_be_merged.
1079  def recheck_merge_status?
1080    self.class.state_machines[:merge_status].check_state?(merge_status)
1081  end
1082
1083  def merge_event
1084    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :merged).last
1085  end
1086
1087  def closed_event
1088    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :closed).last
1089  end
1090
1091  def work_in_progress?
1092    self.class.work_in_progress?(title)
1093  end
1094  alias_method :draft?, :work_in_progress?
1095
1096  def wipless_title
1097    self.class.wipless_title(self.title)
1098  end
1099
1100  def wip_title
1101    self.class.wip_title(self.title)
1102  end
1103
1104  def mergeable?(skip_ci_check: false, skip_discussions_check: false)
1105    return false unless mergeable_state?(skip_ci_check: skip_ci_check,
1106                                         skip_discussions_check: skip_discussions_check)
1107
1108    check_mergeability
1109
1110    can_be_merged? && !should_be_rebased?
1111  end
1112
1113  # rubocop: disable CodeReuse/ServiceClass
1114  def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
1115    return false unless open?
1116    return false if work_in_progress?
1117    return false if broken?
1118    return false unless skip_discussions_check || mergeable_discussions_state?
1119
1120    if Feature.enabled?(:improved_mergeability_checks, self.project, default_enabled: :yaml)
1121      additional_checks = MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: { skip_ci_check: skip_ci_check })
1122      additional_checks.execute.all?(&:success?)
1123    else
1124      return false unless skip_ci_check || mergeable_ci_state?
1125
1126      true
1127    end
1128  end
1129  # rubocop: enable CodeReuse/ServiceClass
1130
1131  def ff_merge_possible?
1132    project.repository.ancestor?(target_branch_sha, diff_head_sha)
1133  end
1134
1135  def should_be_rebased?
1136    project.ff_merge_must_be_possible? && !ff_merge_possible?
1137  end
1138
1139  def can_cancel_auto_merge?(current_user)
1140    can_be_merged_by?(current_user) || self.author == current_user
1141  end
1142
1143  def can_remove_source_branch?(current_user)
1144    source_project &&
1145      !ProtectedBranch.protected?(source_project, source_branch) &&
1146      !source_project.root_ref?(source_branch) &&
1147      Ability.allowed?(current_user, :push_code, source_project) &&
1148      diff_head_sha == source_branch_head.try(:sha)
1149  end
1150
1151  def should_remove_source_branch?
1152    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
1153  end
1154
1155  def force_remove_source_branch?
1156    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
1157  end
1158
1159  def auto_merge_strategy
1160    return unless auto_merge_enabled?
1161
1162    merge_params['auto_merge_strategy'] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
1163  end
1164
1165  def auto_merge_strategy=(strategy)
1166    merge_params['auto_merge_strategy'] = strategy
1167  end
1168
1169  def remove_source_branch?
1170    should_remove_source_branch? || force_remove_source_branch?
1171  end
1172
1173  def notify_conflict?
1174    (opened? || locked?) &&
1175      has_commits? &&
1176      !branch_missing? &&
1177      !project.repository.can_be_merged?(diff_head_sha, target_branch)
1178  rescue Gitlab::Git::CommandError
1179    # Checking mergeability can trigger exception, e.g. non-utf8
1180    # We ignore this type of errors.
1181    false
1182  end
1183
1184  def related_notes
1185    # We're using a UNION ALL here since this results in better performance
1186    # compared to using OR statements. We're using UNION ALL since the queries
1187    # used won't produce any duplicates (e.g. a note for a commit can't also be
1188    # a note for an MR).
1189    Note
1190      .from_union([notes, commit_notes], remove_duplicates: false)
1191      .includes(:noteable)
1192  end
1193
1194  alias_method :discussion_notes, :related_notes
1195
1196  def commit_notes
1197    # Fetch comments only from last 100 commits
1198    commit_ids = commit_shas(limit: 100)
1199
1200    Note
1201      .user
1202      .where(project_id: [source_project_id, target_project_id])
1203      .for_commit_id(commit_ids)
1204  end
1205
1206  def mergeable_discussions_state?
1207    return true unless project.only_allow_merge_if_all_discussions_are_resolved?
1208
1209    unresolved_notes.none?(&:to_be_resolved?)
1210  end
1211
1212  def for_fork?
1213    target_project != source_project
1214  end
1215
1216  def for_same_project?
1217    target_project == source_project
1218  end
1219
1220  # If the merge request closes any issues, save this information in the
1221  # `MergeRequestsClosingIssues` model. This is a performance optimization.
1222  # Calculating this information for a number of merge requests requires
1223  # running `ReferenceExtractor` on each of them separately.
1224  # This optimization does not apply to issues from external sources.
1225  def cache_merge_request_closes_issues!(current_user = self.author)
1226    return unless project.issues_enabled?
1227    return if closed? || merged?
1228
1229    transaction do
1230      self.merge_requests_closing_issues.delete_all
1231
1232      closes_issues(current_user).each do |issue|
1233        next if issue.is_a?(ExternalIssue)
1234
1235        self.merge_requests_closing_issues.create!(issue: issue)
1236      end
1237    end
1238  end
1239
1240  def visible_closing_issues_for(current_user = self.author)
1241    strong_memoize(:visible_closing_issues_for) do
1242      if self.target_project.has_external_issue_tracker?
1243        closes_issues(current_user)
1244      else
1245        cached_closes_issues.select do |issue|
1246          Ability.allowed?(current_user, :read_issue, issue)
1247        end
1248      end
1249    end
1250  end
1251
1252  # Return the set of issues that will be closed if this merge request is accepted.
1253  def closes_issues(current_user = self.author)
1254    if target_branch == project.default_branch
1255      messages = [title, description]
1256      messages.concat(commits.map(&:safe_message)) if merge_request_diff.persisted?
1257
1258      Gitlab::ClosingIssueExtractor.new(project, current_user)
1259        .closed_by_message(messages.join("\n"))
1260    else
1261      []
1262    end
1263  end
1264
1265  def issues_mentioned_but_not_closing(current_user)
1266    return [] unless target_branch == project.default_branch
1267
1268    ext = Gitlab::ReferenceExtractor.new(project, current_user)
1269    ext.analyze("#{title}\n#{description}")
1270
1271    ext.issues - visible_closing_issues_for(current_user)
1272  end
1273
1274  def target_project_path
1275    if target_project
1276      target_project.full_path
1277    else
1278      "(removed)"
1279    end
1280  end
1281
1282  def source_project_path
1283    if source_project
1284      source_project.full_path
1285    else
1286      "(removed)"
1287    end
1288  end
1289
1290  def source_project_namespace
1291    if source_project && source_project.namespace
1292      source_project.namespace.full_path
1293    else
1294      "(removed)"
1295    end
1296  end
1297
1298  def target_project_namespace
1299    if target_project && target_project.namespace
1300      target_project.namespace.full_path
1301    else
1302      "(removed)"
1303    end
1304  end
1305
1306  def source_branch_exists?
1307    return false unless self.source_project
1308
1309    self.source_project.repository.branch_exists?(self.source_branch)
1310  end
1311
1312  def target_branch_exists?
1313    return false unless self.target_project
1314
1315    self.target_project.repository.branch_exists?(self.target_branch)
1316  end
1317
1318  def default_merge_commit_message(include_description: false)
1319    if self.target_project.merge_commit_template.present? && !include_description
1320      return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self).merge_message
1321    end
1322
1323    closes_issues_references = visible_closing_issues_for.map do |issue|
1324      issue.to_reference(target_project)
1325    end
1326
1327    message = [
1328      "Merge branch '#{source_branch}' into '#{target_branch}'",
1329      title
1330    ]
1331
1332    if !include_description && closes_issues_references.present?
1333      message << "Closes #{closes_issues_references.to_sentence}"
1334    end
1335
1336    message << "#{description}" if include_description && description.present?
1337    message << "See merge request #{to_reference(full: true)}"
1338
1339    message.join("\n\n")
1340  end
1341
1342  def default_squash_commit_message
1343    if self.target_project.squash_commit_template.present?
1344      return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self).squash_message
1345    end
1346
1347    title
1348  end
1349
1350  # Returns the oldest multi-line commit
1351  def first_multiline_commit
1352    strong_memoize(:first_multiline_commit) do
1353      recent_commits.without_merge_commits.reverse_each.find(&:description?)
1354    end
1355  end
1356
1357  def squash_on_merge?
1358    return true if target_project.squash_always?
1359    return false if target_project.squash_never?
1360
1361    squash?
1362  end
1363
1364  def has_ci?
1365    return false if has_no_commits?
1366
1367    ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do
1368      !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_integration)
1369    end
1370  end
1371
1372  def branch_missing?
1373    !source_branch_exists? || !target_branch_exists?
1374  end
1375
1376  def broken?
1377    has_no_commits? || branch_missing? || cannot_be_merged?
1378  end
1379
1380  def can_be_merged_by?(user, skip_collaboration_check: false)
1381    access = ::Gitlab::UserAccess.new(user, container: project, skip_collaboration_check: skip_collaboration_check)
1382    access.can_update_branch?(target_branch)
1383  end
1384
1385  def can_be_merged_via_command_line_by?(user)
1386    access = ::Gitlab::UserAccess.new(user, container: project)
1387    access.can_push_to_branch?(target_branch)
1388  end
1389
1390  def mergeable_ci_state?
1391    return true unless project.only_allow_merge_if_pipeline_succeeds?
1392    return false unless actual_head_pipeline
1393    return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped?
1394
1395    actual_head_pipeline.success?
1396  end
1397
1398  def environments_for(current_user, latest: false)
1399    return [] unless diff_head_commit
1400
1401    envs = Environments::EnvironmentsByDeploymentsFinder.new(target_project, current_user,
1402      ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute
1403
1404    if source_project
1405      envs.concat Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user,
1406        ref: source_branch, commit: diff_head_commit, find_latest: latest).execute
1407    end
1408
1409    envs.uniq
1410  end
1411
1412  ##
1413  # This method is for looking for active environments which created via pipelines for merge requests.
1414  # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
1415  # we cannot look up environments with source branch name.
1416  def environments
1417    return Environment.none unless actual_head_pipeline&.merge_request?
1418
1419    build_for_actual_head_pipeline = Ci::Build.latest.where(pipeline: actual_head_pipeline)
1420
1421    environments = build_for_actual_head_pipeline.joins(:metadata)
1422                                                .where.not('ci_builds_metadata.expanded_environment_name' => nil)
1423                                                .distinct('ci_builds_metadata.expanded_environment_name')
1424                                                .limit(100)
1425                                                .pluck(:expanded_environment_name)
1426
1427    Environment.where(project: project, name: environments)
1428  end
1429
1430  def fetch_ref!
1431    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
1432  end
1433
1434  # Returns the current merge-ref HEAD commit.
1435  #
1436  def merge_ref_head
1437    return project.repository.commit(merge_ref_sha) if merge_ref_sha
1438
1439    project.repository.commit(merge_ref_path)
1440  end
1441
1442  def ref_path
1443    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
1444  end
1445
1446  def merge_ref_path
1447    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge"
1448  end
1449
1450  def train_ref_path
1451    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/train"
1452  end
1453
1454  def cleanup_refs(only: :all)
1455    target_refs = []
1456    target_refs << ref_path       if %i[all head].include?(only)
1457    target_refs << merge_ref_path if %i[all merge].include?(only)
1458    target_refs << train_ref_path if %i[all train].include?(only)
1459
1460    project.repository.delete_refs(*target_refs)
1461  end
1462
1463  def self.merge_request_ref?(ref)
1464    ref.start_with?("refs/#{Repository::REF_MERGE_REQUEST}/")
1465  end
1466
1467  def self.merge_train_ref?(ref)
1468    %r{\Arefs/#{Repository::REF_MERGE_REQUEST}/\d+/train\z}.match?(ref)
1469  end
1470
1471  def in_locked_state
1472    lock_mr
1473    yield
1474  ensure
1475    unlock_mr
1476  end
1477
1478  def update_and_mark_in_progress_merge_commit_sha(commit_id)
1479    self.update(in_progress_merge_commit_sha: commit_id)
1480    # Since another process checks for matching merge request, we need
1481    # to make it possible to detect whether the query should go to the
1482    # primary.
1483    target_project.mark_primary_write_location
1484  end
1485
1486  def diverged_commits_count
1487    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
1488
1489    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
1490      cache = {
1491        source_sha: source_branch_sha,
1492        target_sha: target_branch_sha,
1493        diverged_commits_count: compute_diverged_commits_count
1494      }
1495      Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
1496    end
1497
1498    cache[:diverged_commits_count]
1499  end
1500
1501  def compute_diverged_commits_count
1502    return 0 unless source_branch_sha && target_branch_sha
1503
1504    target_project.repository
1505      .count_commits_between(source_branch_sha, target_branch_sha)
1506  end
1507  private :compute_diverged_commits_count
1508
1509  def diverged_from_target_branch?
1510    diverged_commits_count > 0
1511  end
1512
1513  def all_pipelines
1514    strong_memoize(:all_pipelines) do
1515      Ci::PipelinesForMergeRequestFinder.new(self, nil).all
1516    end
1517  end
1518
1519  def update_head_pipeline
1520    find_actual_head_pipeline.try do |pipeline|
1521      self.head_pipeline = pipeline
1522      update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed?
1523    end
1524  end
1525
1526  def has_test_reports?
1527    actual_head_pipeline&.has_reports?(Ci::JobArtifact.test_reports)
1528  end
1529
1530  def predefined_variables
1531    Gitlab::Ci::Variables::Collection.new.tap do |variables|
1532      variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
1533      variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
1534      variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', value: ref_path.to_s)
1535      variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', value: project.id.to_s)
1536      variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path)
1537      variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
1538      variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
1539      variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
1540      variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present?
1541      variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
1542      variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
1543      variables.concat(source_project_variables)
1544    end
1545  end
1546
1547  def compare_test_reports
1548    unless has_test_reports?
1549      return { status: :error, status_reason: 'This merge request does not have test reports' }
1550    end
1551
1552    compare_reports(Ci::CompareTestReportsService)
1553  end
1554
1555  def has_accessibility_reports?
1556    actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports)
1557  end
1558
1559  def has_coverage_reports?
1560    actual_head_pipeline&.has_coverage_reports?
1561  end
1562
1563  def has_terraform_reports?
1564    actual_head_pipeline&.has_reports?(Ci::JobArtifact.terraform_reports)
1565  end
1566
1567  def compare_accessibility_reports
1568    unless has_accessibility_reports?
1569      return { status: :error, status_reason: _('This merge request does not have accessibility reports') }
1570    end
1571
1572    compare_reports(Ci::CompareAccessibilityReportsService)
1573  end
1574
1575  # TODO: this method and compare_test_reports use the same
1576  # result type, which is handled by the controller's #reports_response.
1577  # we should minimize mistakes by isolating the common parts.
1578  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
1579  def find_coverage_reports
1580    unless has_coverage_reports?
1581      return { status: :error, status_reason: 'This merge request does not have coverage reports' }
1582    end
1583
1584    compare_reports(Ci::GenerateCoverageReportsService)
1585  end
1586
1587  def has_codequality_mr_diff_report?
1588    actual_head_pipeline&.has_codequality_mr_diff_report?
1589  end
1590
1591  # TODO: this method and compare_test_reports use the same
1592  # result type, which is handled by the controller's #reports_response.
1593  # we should minimize mistakes by isolating the common parts.
1594  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
1595  def find_codequality_mr_diff_reports
1596    unless has_codequality_mr_diff_report?
1597      return { status: :error, status_reason: 'This merge request does not have codequality mr diff reports' }
1598    end
1599
1600    compare_reports(Ci::GenerateCodequalityMrDiffReportService)
1601  end
1602
1603  def has_codequality_reports?
1604    actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports)
1605  end
1606
1607  def compare_codequality_reports
1608    unless has_codequality_reports?
1609      return { status: :error, status_reason: _('This merge request does not have codequality reports') }
1610    end
1611
1612    compare_reports(Ci::CompareCodequalityReportsService)
1613  end
1614
1615  def find_terraform_reports
1616    unless has_terraform_reports?
1617      return { status: :error, status_reason: 'This merge request does not have terraform reports' }
1618    end
1619
1620    compare_reports(Ci::GenerateTerraformReportsService)
1621  end
1622
1623  def has_exposed_artifacts?
1624    actual_head_pipeline&.has_exposed_artifacts?
1625  end
1626
1627  # TODO: this method and compare_test_reports use the same
1628  # result type, which is handled by the controller's #reports_response.
1629  # we should minimize mistakes by isolating the common parts.
1630  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
1631  def find_exposed_artifacts
1632    unless has_exposed_artifacts?
1633      return { status: :error, status_reason: 'This merge request does not have exposed artifacts' }
1634    end
1635
1636    compare_reports(Ci::GenerateExposedArtifactsReportService)
1637  end
1638
1639  # TODO: consider renaming this as with exposed artifacts we generate reports,
1640  # not always compare
1641  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
1642  def compare_reports(service_class, current_user = nil, report_type = nil )
1643    with_reactive_cache(service_class.name, current_user&.id, report_type) do |data|
1644      unless service_class.new(project, current_user, id: id, report_type: report_type)
1645        .latest?(comparison_base_pipeline(service_class.name), actual_head_pipeline, data)
1646        raise InvalidateReactiveCache
1647      end
1648
1649      data
1650    end || { status: :parsing }
1651  end
1652
1653  def has_sast_reports?
1654    !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports)
1655  end
1656
1657  def has_secret_detection_reports?
1658    !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports)
1659  end
1660
1661  def compare_sast_reports(current_user)
1662    return missing_report_error("SAST") unless has_sast_reports?
1663
1664    compare_reports(::Ci::CompareSecurityReportsService, current_user, 'sast')
1665  end
1666
1667  def compare_secret_detection_reports(current_user)
1668    return missing_report_error("secret detection") unless has_secret_detection_reports?
1669
1670    compare_reports(::Ci::CompareSecurityReportsService, current_user, 'secret_detection')
1671  end
1672
1673  def calculate_reactive_cache(identifier, current_user_id = nil, report_type = nil, *args)
1674    service_class = identifier.constantize
1675
1676    # TODO: the type check should change to something that includes exposed artifacts service
1677    # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
1678    raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
1679
1680    current_user = User.find_by(id: current_user_id)
1681    service_class.new(project, current_user, id: id, report_type: report_type).execute(comparison_base_pipeline(identifier), actual_head_pipeline)
1682  end
1683
1684  def recent_diff_head_shas(limit = 100)
1685    merge_request_diffs.recent(limit).pluck(:head_commit_sha)
1686  end
1687
1688  def all_commits
1689    MergeRequestDiffCommit
1690      .where(merge_request_diff: merge_request_diffs.recent)
1691      .limit(10_000)
1692  end
1693
1694  # Note that this could also return SHA from now dangling commits
1695  #
1696  def all_commit_shas
1697    @all_commit_shas ||= begin
1698      return commit_shas unless persisted?
1699
1700      all_commits.pluck(:sha).uniq
1701    end
1702  end
1703
1704  def merge_commit
1705    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
1706  end
1707
1708  def short_merge_commit_sha
1709    Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
1710  end
1711
1712  def merged_commit_sha
1713    return unless merged?
1714
1715    sha = merge_commit_sha || squash_commit_sha || diff_head_sha
1716    sha.presence
1717  end
1718
1719  def short_merged_commit_sha
1720    if sha = merged_commit_sha
1721      Commit.truncate_sha(sha)
1722    end
1723  end
1724
1725  def can_be_reverted?(current_user)
1726    return false unless merge_commit
1727    return false unless merged_at
1728
1729    # It is not guaranteed that Note#created_at will be strictly later than
1730    # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
1731    # comparison, as will a HA environment if clocks are not *precisely*
1732    # synchronized. Add a minute's leeway to compensate for both possibilities
1733    cutoff = merged_at - 1.minute
1734
1735    notes_association = notes_with_associations.where('created_at >= ?', cutoff)
1736
1737    !merge_commit.has_been_reverted?(current_user, notes_association)
1738  end
1739
1740  def merged_at
1741    strong_memoize(:merged_at) do
1742      next unless merged?
1743
1744      metrics&.merged_at ||
1745        merge_event&.created_at ||
1746        resource_state_events.find_by(state: :merged)&.created_at ||
1747        notes.system.reorder(nil).find_by(note: 'merged')&.created_at
1748    end
1749  end
1750
1751  def can_be_cherry_picked?
1752    merge_commit.present?
1753  end
1754
1755  def has_complete_diff_refs?
1756    diff_refs && diff_refs.complete?
1757  end
1758
1759  # rubocop: disable CodeReuse/ServiceClass
1760  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
1761    return unless has_complete_diff_refs?
1762    return if new_diff_refs == old_diff_refs
1763
1764    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
1765      discussion.active?(old_diff_refs)
1766    end
1767    return if active_diff_discussions.empty?
1768
1769    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
1770
1771    service = Discussions::UpdateDiffPositionService.new(
1772      self.project,
1773      current_user,
1774      old_diff_refs: old_diff_refs,
1775      new_diff_refs: new_diff_refs,
1776      paths: paths
1777    )
1778
1779    active_diff_discussions.each do |discussion|
1780      service.execute(discussion)
1781    end
1782
1783    if project.resolve_outdated_diff_discussions?
1784      MergeRequests::ResolvedDiscussionNotificationService
1785        .new(project: project, current_user: current_user)
1786        .execute(self)
1787    end
1788  end
1789  # rubocop: enable CodeReuse/ServiceClass
1790
1791  def keep_around_commit
1792    project.repository.keep_around(self.merge_commit_sha)
1793  end
1794
1795  def has_commits?
1796    merge_request_diff.persisted? && commits_count.to_i > 0
1797  end
1798
1799  def has_no_commits?
1800    !has_commits?
1801  end
1802
1803  def pipeline_coverage_delta
1804    if base_pipeline&.coverage && head_pipeline&.coverage
1805      head_pipeline.coverage - base_pipeline.coverage
1806    end
1807  end
1808
1809  def use_merge_base_pipeline_for_comparison?(service_class)
1810    ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON[service_class]&.call(project)
1811  end
1812
1813  def comparison_base_pipeline(service_class)
1814    (use_merge_base_pipeline_for_comparison?(service_class) && merge_base_pipeline) || base_pipeline
1815  end
1816
1817  def base_pipeline
1818    @base_pipeline ||= project.ci_pipelines
1819      .order(id: :desc)
1820      .find_by(sha: diff_base_sha, ref: target_branch)
1821  end
1822
1823  def merge_base_pipeline
1824    @merge_base_pipeline ||= project.ci_pipelines
1825      .order(id: :desc)
1826      .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch)
1827  end
1828
1829  def discussions_rendered_on_frontend?
1830    true
1831  end
1832
1833  # rubocop: disable CodeReuse/ServiceClass
1834  def update_project_counter_caches
1835    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
1836  end
1837  # rubocop: enable CodeReuse/ServiceClass
1838
1839  def first_contribution?
1840    return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
1841
1842    !project.merge_requests.merged.exists?(author_id: author_id)
1843  end
1844
1845  # TODO: remove once production database rename completes
1846  # https://gitlab.com/gitlab-org/gitlab-foss/issues/47592
1847  alias_attribute :allow_collaboration, :allow_maintainer_to_push
1848
1849  def allow_collaboration
1850    collaborative_push_possible? && allow_maintainer_to_push
1851  end
1852
1853  alias_method :allow_collaboration?, :allow_collaboration
1854
1855  def collaborative_push_possible?
1856    source_project.present? && for_fork? &&
1857      target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
1858      source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
1859      !ProtectedBranch.protected?(source_project, source_branch)
1860  end
1861
1862  def can_allow_collaboration?(user)
1863    collaborative_push_possible? &&
1864      Ability.allowed?(user, :push_code, source_project)
1865  end
1866
1867  def find_actual_head_pipeline
1868    ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do
1869      all_pipelines.for_sha_or_source_sha(diff_head_sha).first
1870    end
1871  end
1872
1873  def etag_caching_enabled?
1874    true
1875  end
1876
1877  def recent_visible_deployments
1878    deployments.visible.includes(:environment).order(id: :desc).limit(10)
1879  end
1880
1881  def banzai_render_context(field)
1882    super.merge(label_url_method: :project_merge_requests_url)
1883  end
1884
1885  override :ensure_metrics
1886  def ensure_metrics
1887    MergeRequest::Metrics.record!(self)
1888  end
1889
1890  def allows_reviewers?
1891    true
1892  end
1893
1894  def allows_multiple_reviewers?
1895    false
1896  end
1897
1898  def supports_assignee?
1899    true
1900  end
1901
1902  def find_assignee(user)
1903    merge_request_assignees.find_by(user_id: user.id)
1904  end
1905
1906  def find_reviewer(user)
1907    merge_request_reviewers.find_by(user_id: user.id)
1908  end
1909
1910  def enabled_reports
1911    {
1912      sast: report_type_enabled?(:sast),
1913      secret_detection: report_type_enabled?(:secret_detection)
1914    }
1915  end
1916
1917  def includes_ci_config?
1918    return false unless diff_stats
1919
1920    diff_stats.map(&:path).include?(project.ci_config_path_or_default)
1921  end
1922
1923  def context_commits_diff
1924    strong_memoize(:context_commits_diff) do
1925      ContextCommitsDiff.new(self)
1926    end
1927  end
1928
1929  def attention_requested_enabled?
1930    Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml)
1931  end
1932
1933  private
1934
1935  def set_draft_status
1936    self.draft = draft?
1937  end
1938
1939  def missing_report_error(report_type)
1940    { status: :error, status_reason: "This merge request does not have #{report_type} reports" }
1941  end
1942
1943  def with_rebase_lock
1944    with_retried_nowait_lock { yield }
1945  end
1946
1947  # If the merge request is idle in transaction or has a SELECT FOR
1948  # UPDATE, we don't want to block indefinitely or this could cause a
1949  # queue of SELECT FOR UPDATE calls. Instead, try to get the lock for
1950  # 5 s before raising an error to the user.
1951  def with_retried_nowait_lock
1952    # Try at most 0.25 + (1.5 * .25) + (1.5^2 * .25) ... (1.5^5 * .25) = 5.2 s to get the lock
1953    Retriable.retriable(on: ActiveRecord::LockWaitTimeout, tries: 6, base_interval: 0.25) do
1954      with_lock('FOR UPDATE NOWAIT') do
1955        yield
1956      end
1957    end
1958  rescue ActiveRecord::LockWaitTimeout => e
1959    Gitlab::ErrorTracking.track_exception(e)
1960    raise RebaseLockTimeout, _('Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later.')
1961  end
1962
1963  def source_project_variables
1964    Gitlab::Ci::Variables::Collection.new.tap do |variables|
1965      break variables unless source_project
1966
1967      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', value: source_project.id.to_s)
1968      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path)
1969      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url)
1970      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s)
1971    end
1972  end
1973
1974  def expire_etag_cache
1975    return unless project.namespace
1976
1977    key = Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(project, self, format: :json)
1978    Gitlab::EtagCaching::Store.new.touch(key)
1979  end
1980
1981  def report_type_enabled?(report_type)
1982    !!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type)
1983  end
1984end
1985
1986MergeRequest.prepend_mod_with('MergeRequest')
1987