merge_request.rb 39.5 KB
Newer Older
1 2
# frozen_string_literal: true

3
class MergeRequest < ActiveRecord::Base
4
  include AtomicInternalId
Shinya Maeda's avatar
Shinya Maeda committed
5
  include IidRoutes
6
  include Issuable
7
  include Noteable
8
  include Referable
9
  include Presentable
10
  include IgnorableColumn
11
  include TimeTrackable
12 13
  include ManualInverseAssociation
  include EachBatch
14
  include ThrottledTouch
15
  include Gitlab::Utils::StrongMemoize
Jan Provaznik's avatar
Jan Provaznik committed
16
  include LabelEventable
17
  include ReactiveCaching
18
  include FromUnion
19 20

  self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
21 22
  self.reactive_cache_refresh_interval = 10.minutes
  self.reactive_cache_lifetime = 10.minutes
23

24 25
  SORTING_PREFERENCE_FIELD = :merge_requests_sort

26
  ignore_column :locked_at,
27 28
                :ref_fetched,
                :deleted_at
29

30 31
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
32
  belongs_to :merge_user, class_name: "User"
33

34 35
  has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }

36
  has_many :merge_request_diffs
37

38
  has_one :merge_request_diff,
39
    -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
40

41 42 43 44 45 46 47 48 49 50 51 52
  belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
  manual_inverse_association :latest_merge_request_diff, :merge_request

  # 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.
  #
  # 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.
53 54
  def merge_request_diff
    fallback = latest_merge_request_diff unless association(:merge_request_diff).loaded?
55 56 57 58

    fallback || super
  end

59 60
  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"

61
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
62

63 64 65
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
66

67
  has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
Shinya Maeda's avatar
Shinya Maeda committed
68
  has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
69

70 71
  belongs_to :assignee, class_name: "User"

72
  serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
73

74
  after_create :ensure_merge_request_diff
75
  after_update :clear_memoized_shas
76
  after_update :reload_diff_if_branch_changed
77
  after_save :ensure_metrics
78

79 80 81 82
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

83 84
  # Temporary fields to store compare vars
  # when creating new merge request
85
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
86

Andrew8xx8's avatar
Andrew8xx8 committed
87
  state_machine :state, initial: :opened do
88
    event :close do
89
      transition [:opened] => :closed
90 91
    end

92
    event :mark_as_merged do
93
      transition [:opened, :locked] => :merged
94 95 96
    end

    event :reopen do
97
      transition closed: :opened
98 99
    end

100
    event :lock_mr do
101
      transition [:opened] => :locked
102 103
    end

104
    event :unlock_mr do
105
      transition locked: :opened
106 107
    end

108 109
    before_transition any => :opened do |merge_request|
      merge_request.merge_jid = nil
110
    end
111

112
    after_transition any => :opened do |merge_request|
113 114 115 116 117
      merge_request.run_after_commit do
        UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
      end
    end

118 119 120
    state :opened
    state :closed
    state :merged
121
    state :locked
122 123
  end

124 125
  state_machine :merge_status, initial: :unchecked do
    event :mark_as_unchecked do
126 127
      transition [:can_be_merged, :unchecked] => :unchecked
      transition [:cannot_be_merged, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
128 129 130
    end

    event :mark_as_mergeable do
131
      transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged
132 133 134
    end

    event :mark_as_unmergeable do
135
      transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged
136 137
    end

138
    state :unchecked
139
    state :cannot_be_merged_recheck
140 141
    state :can_be_merged
    state :cannot_be_merged
142 143

    around_transition do |merge_request, transition, block|
144
      Gitlab::Timeless.timeless(merge_request, &block)
145
    end
146

147
    # rubocop: disable CodeReuse/ServiceClass
148
    after_transition unchecked: :cannot_be_merged do |merge_request, transition|
149 150 151
      if merge_request.notify_conflict?
        NotificationService.new.merge_request_unmergeable(merge_request)
        TodoService.new.merge_request_became_unmergeable(merge_request)
152
      end
153
    end
154
    # rubocop: enable CodeReuse/ServiceClass
155

156 157 158
    def check_state?(merge_status)
      [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
    end
159
  end
160

161
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
162
  validates :source_branch, presence: true
163
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
164
  validates :target_branch, presence: true
165
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
166 167
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
168
  validate :validate_target_project, on: :create
169

170 171 172
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
173
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
174
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
175
  scope :from_project, ->(project) { where(source_project_id: project.id) }
176 177
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
178
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
179 180 181
  scope :by_commit_sha, ->(sha) do
    where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
  end
182 183
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
184 185 186
  scope :assigned, -> { where("assignee_id IS NOT NULL") }
  scope :unassigned, -> { where("assignee_id IS NULL") }
  scope :assigned_to, ->(u) { where(assignee_id: u.id)}
187 188 189 190 191 192 193
  scope :with_api_entity_associations, -> {
    preload(:author, :assignee, :notes, :labels, :milestone, :timelogs,
            latest_merge_request_diff: [:merge_request_diff_commits],
            metrics: [:latest_closed_by, :merged_by],
            target_project: [:route, { namespace: :route }],
            source_project: [:route, { namespace: :route }])
  }
194 195

  participant :assignee
196

197 198
  after_save :keep_around_commit

199 200 201
  alias_attribute :project, :target_project
  alias_attribute :project_id, :target_project_id

202 203 204 205
  def self.reference_prefix
    '!'
  end

206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
  # 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.
  def self.recent_target_branches(limit: 100)
    group(:target_branch)
      .select(:target_branch)
      .reorder('MAX(merge_requests.updated_at) DESC')
      .limit(limit)
      .pluck(:target_branch)
  end

222
  def rebase_in_progress?
223 224 225
    strong_memoize(:rebase_in_progress) do
      # The source project can be deleted
      next false unless source_project
226

227 228
      source_project.repository.rebase_in_progress?(id)
    end
229 230
  end

231 232 233
  # 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: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
234
  def actual_head_pipeline
235
    head_pipeline&.matches_sha_or_source_sha?(diff_head_sha) ? head_pipeline : nil
236 237
  end

238 239 240 241 242 243
  def merge_pipeline
    return unless merged?

    target_project.pipeline_for(target_branch, merge_commit_sha)
  end

244 245 246 247
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
248
    @reference_pattern ||= %r{
249
      (#{Project.reference_pattern})?
250 251 252 253
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

254
  def self.link_reference_pattern
255
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
256 257
  end

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

262 263 264 265
  def self.project_foreign_key
    'target_project_id'
  end

266 267 268 269 270 271 272 273 274 275 276
  # 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.
  def self.in_projects(relation)
277 278
    # unscoping unnecessary conditions that'll be applied
    # when executing `where("merge_requests.id IN (#{union.to_sql})")`
279 280
    source = unscoped.where(source_project_id: relation)
    target = unscoped.where(target_project_id: relation)
281

282
    from_union([source, target])
283 284
  end

285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  # 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.
  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
      )'.squish

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

301
  WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
302 303 304 305 306 307 308 309 310 311 312 313 314

  def self.work_in_progress?(title)
    !!(title =~ WIP_REGEX)
  end

  def self.wipless_title(title)
    title.sub(WIP_REGEX, "")
  end

  def self.wip_title(title)
    work_in_progress?(title) ? title : "WIP: #{title}"
  end

315 316
  def commit_authors
    @commit_authors ||= commits.authors
317 318 319
  end

  def authors
320
    User.from_union([commit_authors, User.where(id: self.author_id)])
321 322
  end

323 324 325 326 327 328
  # Verifies if title has changed not taking into account WIP prefix
  # for merge requests.
  def wipless_title_changed(old_title)
    self.class.wipless_title(old_title) != self.wipless_title
  end

329
  def hook_attrs
330
    Gitlab::HookData::MergeRequestBuilder.new(self).build
331 332
  end

333 334 335 336 337 338 339 340
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

341
  # These method are needed for compatibility with issues to not mess view and other code
342 343 344 345
  def assignees
    Array(assignee)
  end

346 347 348 349 350 351 352 353
  def assignee_ids
    Array(assignee_id)
  end

  def assignee_ids=(ids)
    write_attribute(:assignee_id, ids.last)
  end

354 355 356 357
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

358
  # `from` argument can be a Namespace or Project.
359
  def to_reference(from = nil, full: false)
360 361
    reference = "#{self.class.reference_prefix}#{iid}"

362
    "#{project.to_reference(from, full: full)}#{reference}"
363 364
  end

365
  def commits
366 367 368 369 370 371 372 373 374
    return merge_request_diff.commits if persisted?

    commits_arr = if compare_commits
                    compare_commits.reverse
                  else
                    []
                  end

    CommitCollection.new(source_project, commits_arr, source_branch)
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
  end

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

  def commit_shas
    if persisted?
      merge_request_diff.commit_shas
    elsif compare_commits
391
      compare_commits.to_a.reverse.map(&:sha)
392
    else
393
      Array(diff_head_sha)
394 395 396
    end
  end

397 398 399 400 401 402 403 404 405
  # Returns true if there are commits that match at least one commit SHA.
  def includes_any_commits?(shas)
    if persisted?
      merge_request_diff.commits_by_shas(shas).exists?
    else
      (commit_shas & shas).present?
    end
  end

406
  def supports_suggestion?
407
    true
408 409
  end

410 411 412
  # Calls `MergeWorker` to proceed with the merge process and
  # updates `merge_jid` with the MergeWorker#jid.
  # This helps tracking enqueued and ongoing merge jobs.
413
  def merge_async(user_id, params)
414
    jid = MergeWorker.perform_async(id, user_id, params.to_h)
415 416 417
    update_column(:merge_jid, jid)
  end

418 419 420 421 422 423 424 425 426 427
  def merge_participants
    participants = [author]

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

    participants
  end

428 429
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
430
  end
431

432
  def raw_diffs(*args)
433
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
434 435
  end

436
  def diffs(diff_options = {})
437
    if compare
438
      # When saving MR diffs, `expanded` is implicitly added (because we need
439 440
      # to save the entire contents to the DB), so add that here for
      # consistency.
441
      compare.diffs(diff_options.merge(expanded: true))
442
    else
443
      merge_request_diff.diffs(diff_options)
444
    end
445 446
  end

447 448 449 450
  def non_latest_diffs
    merge_request_diffs.where.not(id: merge_request_diff.id)
  end

451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
  def preloads_discussion_diff_highlighting?
    true
  end

  def preload_discussions_diff_highlight
    preloadable_files = note_diff_files.for_commit_or_unresolved

    discussions_diffs.load_highlight(preloadable_files.pluck(:id))
  end

  def discussions_diffs
    strong_memoize(:discussions_diffs) do
      Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
    end
  end

  def note_diff_files
    NoteDiffFile
      .where(diff_note: discussion_notes)
      .includes(diff_note: :project)
  end

473
  def diff_size
474 475
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
476
    merge_request_diff&.real_size || diffs.real_size
477 478
  end

479 480 481 482 483 484 485 486 487 488 489 490
  def modified_paths(past_merge_request_diff: nil)
    diffs = if past_merge_request_diff
              past_merge_request_diff
            elsif compare
              compare
            else
              self.merge_request_diff
            end

    diffs.modified_paths
  end

491
  def diff_base_commit
492
    if persisted?
493
      merge_request_diff.base_commit
494 495
    else
      branch_merge_base_commit
496 497 498 499 500 501 502 503
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
504 505 506
    end
  end

507 508 509 510 511 512 513 514 515
  def diff_head_commit
    if persisted?
      merge_request_diff.head_commit
    else
      source_branch_head
    end
  end

  def diff_start_sha
516 517 518 519 520
    if persisted?
      merge_request_diff.start_commit_sha
    else
      target_branch_head.try(:sha)
    end
521 522 523
  end

  def diff_base_sha
524 525 526 527 528
    if persisted?
      merge_request_diff.base_commit_sha
    else
      branch_merge_base_commit.try(:sha)
    end
529 530 531
  end

  def diff_head_sha
532 533 534 535 536
    if persisted?
      merge_request_diff.head_commit_sha
    else
      source_branch_head.try(:sha)
    end
537 538 539 540 541 542 543 544
  end

  # When importing a pull request from GitHub, the old and new branches may no
  # longer actually exist by those names, but we need to recreate the merge
  # request diff with the right source and target shas.
  # We use these attributes to force these to the intended values.
  attr_writer :target_branch_sha, :source_branch_sha

545 546 547 548 549 550 551 552 553 554 555 556 557 558
  def source_branch_ref
    return @source_branch_sha if @source_branch_sha
    return unless source_branch

    Gitlab::Git::BRANCH_REF_PREFIX + source_branch
  end

  def target_branch_ref
    return @target_branch_sha if @target_branch_sha
    return unless target_branch

    Gitlab::Git::BRANCH_REF_PREFIX + target_branch
  end

559
  def source_branch_head
560 561 562 563 564
    strong_memoize(:source_branch_head) do
      if source_project && source_branch_ref
        source_project.repository.commit(source_branch_ref)
      end
    end
565 566 567
  end

  def target_branch_head
568 569 570
    strong_memoize(:target_branch_head) do
      target_project.repository.commit(target_branch_ref)
    end
571 572
  end

573 574 575 576 577 578 579 580 581
  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

582
  def target_branch_sha
583
    @target_branch_sha || target_branch_head.try(:sha)
584 585 586
  end

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

590
  def diff_refs
591 592 593 594 595 596 597 598 599 600 601 602 603
    persisted? ? merge_request_diff.diff_refs : repository_diff_refs
  end

  # Instead trying to fetch the
  # persisted diff_refs, this method goes
  # straight to the repository to get the
  # most recent data possible.
  def repository_diff_refs
    Gitlab::Diff::DiffRefs.new(
      base_sha:  branch_merge_base_sha,
      start_sha: target_branch_sha,
      head_sha:  source_branch_sha
    )
604 605
  end

606 607 608 609
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

610
  def validate_branches
611
    if target_project == source_project && target_branch == source_branch
612 613
      errors.add :branch_conflict, "You can't use same project/branch for source and target"
      return
614
    end
615

616
    if opened?
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
      similar_mrs = target_project
        .merge_requests
        .where(source_branch: source_branch, target_branch: target_branch)
        .where(source_project_id: source_project&.id)
        .opened

      similar_mrs = similar_mrs.where.not(id: id) if persisted?

      conflict = similar_mrs.first

      if conflict.present?
        errors.add(
          :validate_branches,
          "Another open merge request already exists for this source branch: #{conflict.to_reference}"
        )
632
      end
633
    end
634 635
  end

636 637 638 639 640 641
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

642
  def validate_fork
643
    return true unless target_project && source_project
644
    return true if target_project == source_project
645
    return true unless source_project_missing?
646

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

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

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

659
  def closed_without_fork?
660
    closed? && source_project_missing?
661 662
  end

663
  def source_project_missing?
664 665 666
    return false unless for_fork?
    return true unless source_project

667
    !source_project.in_fork_network_of?(target_project)
668 669
  end

670
  def reopenable?
671
    closed? && !source_project_missing? && source_branch_exists?
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
672 673
  end

674 675
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
676 677
  end

678
  def create_merge_request_diff
679
    fetch_ref!
680

681 682 683 684 685
    # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
    Gitlab::GitalyClient.allow_n_plus_1_calls do
      merge_request_diffs.create
      reload_merge_request_diff
    end
686 687
  end

688 689 690 691
  def viewable_diffs
    @viewable_diffs ||= merge_request_diffs.viewable.to_a
  end

692
  def merge_request_diff_for(diff_refs_or_sha)
693 694 695 696 697 698 699 700 701 702
    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
Douwe Maan's avatar
Douwe Maan committed
703

704 705 706
    viewable_diffs.find do |diff|
      diff.attributes.slice(*matcher.keys) == matcher
    end
707 708
  end

709 710 711 712 713 714 715 716 717 718 719
  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

720 721 722 723 724 725 726
  def clear_memoized_shas
    @target_branch_sha = @source_branch_sha = nil

    clear_memoization(:source_branch_head)
    clear_memoization(:target_branch_head)
  end

727
  def reload_diff_if_branch_changed
728 729
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
730
      reload_diff
731 732 733
    end
  end

734
  # rubocop: disable CodeReuse/ServiceClass
735
  def reload_diff(current_user = nil)
736 737
    return unless open?

738
    MergeRequests::ReloadDiffsService.new(self, current_user).execute
739
  end
740
  # rubocop: enable CodeReuse/ServiceClass
741

742
  def check_if_can_be_merged
743
    return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
744

745
    can_be_merged =
746
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
747 748

    if can_be_merged
749 750 751 752
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
753 754
  end

755
  def merge_event
756
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
757 758
  end

759
  def closed_event
760
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
761 762
  end

763
  def work_in_progress?
764
    self.class.work_in_progress?(title)
765 766 767
  end

  def wipless_title
768 769 770 771 772
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
773 774
  end

775 776
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
777 778 779

    check_if_can_be_merged

780
    can_be_merged? && !should_be_rebased?
781 782
  end

783
  def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
784 785 786
    return false unless open?
    return false if work_in_progress?
    return false if broken?
787
    return false unless skip_ci_check || mergeable_ci_state?
788
    return false unless skip_discussions_check || mergeable_discussions_state?
789 790

    true
791 792
  end

793 794 795 796 797 798 799 800 801 802
  def mergeable_to_ref?
    return false if merged?
    return false if broken?

    # Given the `merge_ref_path` will have the same
    # state the `target_branch` would have. Ideally
    # we need to check if it can be merged to it.
    project.repository.can_be_merged?(diff_head_sha, target_branch)
  end

803 804 805 806 807 808 809 810
  def ff_merge_possible?
    project.repository.ancestor?(target_branch_sha, diff_head_sha)
  end

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

811
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
812
    can_be_merged_by?(current_user) || self.author == current_user
813 814
  end

815
  def can_remove_source_branch?(current_user)
816
    !ProtectedBranch.protected?(source_project, source_branch) &&
817
      !source_project.root_ref?(source_branch) &&
818
      Ability.allowed?(current_user, :push_code, source_project) &&
819
      diff_head_sha == source_branch_head.try(:sha)
820 821
  end

822
  def should_remove_source_branch?
823
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
824 825 826
  end

  def force_remove_source_branch?
827
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
828 829 830 831 832 833
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

834
  def notify_conflict?
835 836 837 838 839 840 841 842
    (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
843 844
  end

845
  def related_notes
846 847
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
848
    commit_ids = commit_shas.take(commits_for_notes_limit)
849

850 851 852
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
853
      .for_commit_id(commit_ids)
854 855 856 857 858

    # 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).
859 860
    Note
      .from_union([notes, commit_notes], remove_duplicates: false)
861
      .includes(:noteable)
862
  end
863

864
  alias_method :discussion_notes, :related_notes
865

866 867 868
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

869
    !discussions_to_be_resolved?
870 871
  end

872 873 874 875
  def for_fork?
    target_project != source_project
  end

876 877 878 879
  # 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.
880
  # This optimization does not apply to issues from external sources.
881
  def cache_merge_request_closes_issues!(current_user = self.author)
882
    return unless project.issues_enabled?
883
    return if closed? || merged?
884

885
    transaction do
886
      self.merge_requests_closing_issues.delete_all
887

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

891
        self.merge_requests_closing_issues.create!(issue: issue)
892 893 894 895
      end
    end
  end

896 897 898 899 900 901 902 903 904 905 906 907
  def visible_closing_issues_for(current_user = self.author)
    strong_memoize(:visible_closing_issues_for) do
      if self.target_project.has_external_issue_tracker?
        closes_issues(current_user)
      else
        cached_closes_issues.select do |issue|
          Ability.allowed?(current_user, :read_issue, issue)
        end
      end
    end
  end

908
  # Return the set of issues that will be closed if this merge request is accepted.
909
  def closes_issues(current_user = self.author)
910
    if target_branch == project.default_branch
911
      messages = [title, description]
912
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
913

914 915
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
916 917 918 919 920
    else
      []
    end
  end

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

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

927
    ext.issues - visible_closing_issues_for(current_user)
928 929
  end

930 931
  def target_project_path
    if target_project
932
      target_project.full_path
933 934 935 936 937 938 939
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
940
      source_project.full_path
941 942 943 944 945
    else
      "(removed)"
    end
  end

946 947
  def source_project_namespace
    if source_project && source_project.namespace
948
      source_project.namespace.full_path
949 950 951 952 953
    else
      "(removed)"
    end
  end

954 955
  def target_project_namespace
    if target_project && target_project.namespace
956
      target_project.namespace.full_path
957 958 959 960 961
    else
      "(removed)"
    end
  end

962 963 964
  def source_branch_exists?
    return false unless self.source_project

965
    self.source_project.repository.branch_exists?(self.source_branch)
966 967 968 969 970
  end

  def target_branch_exists?
    return false unless self.target_project

971
    self.target_project.repository.branch_exists?(self.target_branch)
972 973
  end

974
  def default_merge_commit_message(include_description: false)
975
    closes_issues_references = visible_closing_issues_for.map do |issue|
976 977 978
      issue.to_reference(target_project)
    end

979 980 981 982
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
983

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

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

991
    message.join("\n\n")
992
  end
993

994 995 996 997 998 999 1000
  # Returns the oldest multi-line commit message, or the MR title if none found
  def default_squash_commit_message
    strong_memoize(:default_squash_commit_message) do
      commits.without_merge_commits.reverse.find(&:description?)&.safe_message || title
    end
  end

1001 1002
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
1003

1004
    self.merge_when_pipeline_succeeds = false
1005
    self.merge_user = nil
1006 1007 1008
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
1009
      merge_params.delete('squash_commit_message')
1010
    end
1011 1012 1013 1014

    self.save
  end

1015
  # Return array of possible target branches
Steven Burgart's avatar
Steven Burgart committed
1016
  # depends on target project of MR
1017 1018 1019 1020 1021 1022 1023 1024 1025
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
Steven Burgart's avatar
Steven Burgart committed
1026
  # depends on source project of MR
1027 1028 1029 1030 1031 1032 1033
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
1034

1035
  def has_ci?
1036
    return false if has_no_commits?
1037

1038
    !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
1039 1040 1041 1042 1043
  end

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

1045
  def broken?
1046
    has_no_commits? || branch_missing? || cannot_be_merged?
1047 1048
  end

1049
  def can_be_merged_by?(user)
1050
    access = ::Gitlab::UserAccess.new(user, project: project)
1051
    access.can_update_branch?(target_branch)
1052 1053 1054 1055 1056
  end

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

1059
  def mergeable_ci_state?
1060
    return true unless project.only_allow_merge_if_pipeline_succeeds?
1061
    return true unless head_pipeline
1062

1063
    actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
1064 1065
  end

Douwe Maan's avatar
Douwe Maan committed
1066
  def environments_for(current_user)
1067
    return [] unless diff_head_commit
1068

Douwe Maan's avatar
Douwe Maan committed
1069 1070 1071
    @environments ||= Hash.new do |h, current_user|
      envs = EnvironmentsFinder.new(target_project, current_user,
        ref: target_branch, commit: diff_head_commit, with_tags: true).execute
1072

Douwe Maan's avatar
Douwe Maan committed
1073 1074 1075 1076
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
1077

Douwe Maan's avatar
Douwe Maan committed
1078
      h[current_user] = envs.uniq
1079
    end
Douwe Maan's avatar
Douwe Maan committed
1080 1081

    @environments[current_user]
1082 1083
  end

1084 1085 1086 1087 1088 1089 1090 1091 1092
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
1093

1094 1095
  def state_icon_name
    if merged?
1096
      "git-merge"
1097
    elsif closed?
1098
      "close"
1099
    else
1100
      "issue-open-m"
1101 1102 1103
    end
  end

1104 1105
  def fetch_ref!
    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
1106 1107
  end

1108
  def ref_path
1109
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
1110 1111
  end

1112 1113 1114 1115
  def merge_ref_path
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge"
  end

1116 1117 1118 1119 1120
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
1121
      unlock_mr
1122 1123
    end
  end
1124

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

1128
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
1129
      cache = {
1130 1131
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
1132 1133 1134 1135 1136 1137 1138 1139 1140
        diverged_commits_count: compute_diverged_commits_count
      }
      Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
    end

    cache[:diverged_commits_count]
  end

  def compute_diverged_commits_count
1141
    return 0 unless source_branch_sha && target_branch_sha
1142

1143 1144
    target_project.repository
      .count_commits_between(source_branch_sha, target_branch_sha)
1145
  end
1146
  private :compute_diverged_commits_count
1147 1148 1149 1150 1151

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

1152
  def all_pipelines
1153
    return Ci::Pipeline.none unless source_project
1154

1155 1156 1157 1158 1159 1160 1161 1162 1163
    shas = all_commit_shas

    strong_memoize(:all_pipelines) do
      Ci::Pipeline.from_union(
        [source_project.ci_pipelines.merge_request_pipelines(self, shas),
         source_project.ci_pipelines.detached_merge_request_pipelines(self, shas),
         source_project.ci_pipelines.triggered_for_branch(source_branch).for_sha(shas)],
         remove_duplicates: false).sort_by_merge_request_pipelines
    end
1164 1165 1166
  end

  def update_head_pipeline
1167 1168 1169 1170
    find_actual_head_pipeline.try do |pipeline|
      self.head_pipeline = pipeline
      update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed?
    end
Shinya Maeda's avatar
Shinya Maeda committed
1171 1172 1173 1174
  end

  def merge_request_pipeline_exists?
    merge_request_pipelines.exists?(sha: diff_head_sha)
1175
  end
1176

1177 1178 1179 1180
  def has_test_reports?
    actual_head_pipeline&.has_test_reports?
  end

1181 1182 1183 1184
  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)
1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
      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_TITLE', value: title)
      variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
      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.concat(source_project_variables)
1195 1196 1197
    end
  end

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

1203 1204 1205 1206 1207 1208
    compare_reports(Ci::CompareTestReportsService)
  end

  def compare_reports(service_class)
    with_reactive_cache(service_class.name) do |data|
      unless service_class.new(project)
1209 1210 1211 1212 1213 1214
        .latest?(base_pipeline, actual_head_pipeline, data)
        raise InvalidateReactiveCache
      end

      data
    end || { status: :parsing }
1215 1216
  end

1217
  def calculate_reactive_cache(identifier, *args)
1218 1219 1220 1221 1222
    service_class = identifier.constantize

    raise NameError, service_class unless service_class < Ci::CompareReportsBaseService

    service_class.new(project).execute(base_pipeline, actual_head_pipeline)
1223 1224
  end

1225
  def all_commits
1226
    # MySQL doesn't support LIMIT in a subquery.
1227 1228 1229 1230 1231
    diffs_relation = if Gitlab::Database.postgresql?
                       merge_request_diffs.recent
                     else
                       merge_request_diffs
                     end
1232

1233 1234 1235
    MergeRequestDiffCommit
      .where(merge_request_diff: diffs_relation)
      .limit(10_000)
1236 1237 1238 1239 1240 1241 1242
  end

  # Note that this could also return SHA from now dangling commits
  #
  def all_commit_shas
    @all_commit_shas ||= begin
      return commit_shas unless persisted?
1243

1244 1245
      all_commits.pluck(:sha).uniq
    end
1246 1247
  end

1248 1249 1250 1251
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

1252 1253 1254 1255
  def short_merge_commit_sha
    Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
  end

1256
  def can_be_reverted?(current_user)
1257
    return false unless merge_commit
1258
    return false unless merged_at
1259

1260 1261 1262 1263 1264
    # 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
1265

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

    !merge_commit.has_been_reverted?(current_user, notes_association)
1269
  end