merge_request.rb 30.9 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
class MergeRequest < ActiveRecord::Base
2
  include AtomicInternalId
3
  include Issuable
4
  include Noteable
5
  include Referable
6
  include IgnorableColumn
7
  include TimeTrackable
8 9
  include ManualInverseAssociation
  include EachBatch
10
  include ThrottledTouch
11
  include Gitlab::Utils::StrongMemoize
12

13
  ignore_column :locked_at,
14 15
                :ref_fetched,
                :deleted_at
16

17 18
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
19
  belongs_to :merge_user, class_name: "User"
20

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

23
  has_many :merge_request_diffs
24

25
  has_one :merge_request_diff,
26
    -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
27

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
  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.
  def merge_request_diff(*args)
    fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?

    fallback || super
  end

46 47
  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"

48
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
49

50 51 52
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
53

54 55
  belongs_to :assignee, class_name: "User"

56
  serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
57

58
  after_create :ensure_merge_request_diff, unless: :importing?
59
  after_update :clear_memoized_shas
60
  after_update :reload_diff_if_branch_changed
61

62 63 64 65
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

66 67
  # Temporary fields to store compare vars
  # when creating new merge request
68
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
69

Andrew8xx8's avatar
Andrew8xx8 committed
70
  state_machine :state, initial: :opened do
71
    event :close do
72
      transition [:opened] => :closed
73 74
    end

75
    event :mark_as_merged do
76
      transition [:opened, :locked] => :merged
77 78 79
    end

    event :reopen do
80
      transition closed: :opened
81 82
    end

83
    event :lock_mr do
84
      transition [:opened] => :locked
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
85 86
    end

87
    event :unlock_mr do
88
      transition locked: :opened
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
89 90
    end

91 92 93 94 95 96 97 98
    before_transition any => :opened do |merge_request|
      merge_request.merge_jid = nil

      merge_request.run_after_commit do
        UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
      end
    end

99 100 101
    state :opened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
102
    state :locked
103 104
  end

105 106
  state_machine :merge_status, initial: :unchecked do
    event :mark_as_unchecked do
107 108
      transition [:can_be_merged] => :unchecked
      transition [:cannot_be_merged] => :cannot_be_merged_recheck
109 110 111
    end

    event :mark_as_mergeable do
112
      transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged
113 114 115
    end

    event :mark_as_unmergeable do
116
      transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged
117 118
    end

119
    state :unchecked
120
    state :cannot_be_merged_recheck
121 122
    state :can_be_merged
    state :cannot_be_merged
123 124

    around_transition do |merge_request, transition, block|
125
      Gitlab::Timeless.timeless(merge_request, &block)
126
    end
127 128 129 130

    def check_state?(merge_status)
      [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
    end
131
  end
132

133
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
134
  validates :source_branch, presence: true
135
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
136
  validates :target_branch, presence: true
137
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
138 139
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
140
  validate :validate_target_project, on: :create
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
141

142 143 144
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
145
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
146
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
147
  scope :from_project, ->(project) { where(source_project_id: project.id) }
148 149
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
150
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
151 152 153
  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
154 155
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
156 157 158 159 160
  scope :assigned, -> { where("assignee_id IS NOT NULL") }
  scope :unassigned, -> { where("assignee_id IS NULL") }
  scope :assigned_to, ->(u) { where(assignee_id: u.id)}

  participant :assignee
161

162 163
  after_save :keep_around_commit

164 165 166 167
  def self.reference_prefix
    '!'
  end

168
  def rebase_in_progress?
169 170 171
    strong_memoize(:rebase_in_progress) do
      # The source project can be deleted
      next false unless source_project
172

173 174
      source_project.repository.rebase_in_progress?(id)
    end
175 176
  end

177 178 179
  # 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
180
  def actual_head_pipeline
181
    head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
182 183
  end

184 185 186 187
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
188
    @reference_pattern ||= %r{
189
      (#{Project.reference_pattern})?
190 191 192 193
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

194
  def self.link_reference_pattern
195
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
196 197
  end

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

202 203 204 205
  def self.project_foreign_key
    'target_project_id'
  end

206 207 208 209 210 211 212 213 214 215 216
  # 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)
217 218 219 220
    # unscoping unnecessary conditions that'll be applied
    # when executing `where("merge_requests.id IN (#{union.to_sql})")`
    source = unscoped.where(source_project_id: relation).select(:id)
    target = unscoped.where(target_project_id: relation).select(:id)
221 222
    union  = Gitlab::SQL::Union.new([source, target])

223
    where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
224 225
  end

226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
  # 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

242 243 244 245 246 247 248 249 250 251 252 253 254 255
  WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze

  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

256 257 258 259 260 261
  # 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

262
  def hook_attrs
263
    Gitlab::HookData::MergeRequestBuilder.new(self).build
264 265
  end

266 267 268 269 270 271 272 273
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

274
  # These method are needed for compatibility with issues to not mess view and other code
275 276 277 278
  def assignees
    Array(assignee)
  end

279 280 281 282 283 284 285 286
  def assignee_ids
    Array(assignee_id)
  end

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

287 288 289 290
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

291
  # `from` argument can be a Namespace or Project.
292
  def to_reference(from = nil, full: false)
293 294
    reference = "#{self.class.reference_prefix}#{iid}"

295
    "#{project.to_reference(from, full: full)}#{reference}"
296 297
  end

298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
  def commits
    if persisted?
      merge_request_diff.commits
    elsif compare_commits
      compare_commits.reverse
    else
      []
    end
  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
322
      compare_commits.to_a.reverse.map(&:sha)
323
    else
324
      Array(diff_head_sha)
325 326 327
    end
  end

328 329 330
  # Calls `MergeWorker` to proceed with the merge process and
  # updates `merge_jid` with the MergeWorker#jid.
  # This helps tracking enqueued and ongoing merge jobs.
331
  def merge_async(user_id, params)
332
    jid = MergeWorker.perform_async(id, user_id, params.to_h)
333 334 335
    update_column(:merge_jid, jid)
  end

336 337 338 339 340 341 342 343 344 345
  def merge_participants
    participants = [author]

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

    participants
  end

346 347
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
348
  end
349

350
  def raw_diffs(*args)
351
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
352 353
  end

354
  def diffs(diff_options = {})
355
    if compare
356
      # When saving MR diffs, `expanded` is implicitly added (because we need
357 358
      # to save the entire contents to the DB), so add that here for
      # consistency.
359
      compare.diffs(diff_options.merge(expanded: true))
360
    else
361
      merge_request_diff.diffs(diff_options)
362
    end
363 364
  end

365
  def diff_size
366 367
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
368
    merge_request_diff&.real_size || diffs.real_size
369 370
  end

371
  def diff_base_commit
372
    if persisted?
373
      merge_request_diff.base_commit
374 375
    else
      branch_merge_base_commit
376 377 378 379 380 381 382 383
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
384 385 386
    end
  end

387 388 389 390 391 392 393 394 395
  def diff_head_commit
    if persisted?
      merge_request_diff.head_commit
    else
      source_branch_head
    end
  end

  def diff_start_sha
396 397 398 399 400
    if persisted?
      merge_request_diff.start_commit_sha
    else
      target_branch_head.try(:sha)
    end
401 402 403
  end

  def diff_base_sha
404 405 406 407 408
    if persisted?
      merge_request_diff.base_commit_sha
    else
      branch_merge_base_commit.try(:sha)
    end
409 410 411
  end

  def diff_head_sha
412 413 414 415 416
    if persisted?
      merge_request_diff.head_commit_sha
    else
      source_branch_head.try(:sha)
    end
417 418 419 420 421 422 423 424
  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

425 426 427 428 429 430 431 432 433 434 435 436 437 438
  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

439
  def source_branch_head
440 441 442 443 444
    strong_memoize(:source_branch_head) do
      if source_project && source_branch_ref
        source_project.repository.commit(source_branch_ref)
      end
    end
445 446 447
  end

  def target_branch_head
448 449 450
    strong_memoize(:target_branch_head) do
      target_project.repository.commit(target_branch_ref)
    end
451 452
  end

453 454 455 456 457 458 459 460 461
  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

462
  def target_branch_sha
463
    @target_branch_sha || target_branch_head.try(:sha)
464 465 466
  end

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

470
  def diff_refs
471
    if persisted?
472
      merge_request_diff.diff_refs
473
    else
474 475 476 477 478
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
479
    end
480 481
  end

482 483 484 485
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

486
  def validate_branches
487
    if target_project == source_project && target_branch == source_branch
488
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
489
    end
490

491
    if opened?
492
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
493 494
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
495
        errors.add :validate_branches,
Gabriel Mazetto's avatar
Gabriel Mazetto committed
496
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
497
      end
498
    end
499 500
  end

501 502 503 504 505 506
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

507
  def validate_fork
508
    return true unless target_project && source_project
509
    return true if target_project == source_project
510
    return true unless source_project_missing?
511

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

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

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

524
  def closed_without_fork?
525
    closed? && source_project_missing?
526 527
  end

528
  def source_project_missing?
529 530 531
    return false unless for_fork?
    return true unless source_project

532
    !source_project.in_fork_network_of?(target_project)
533 534
  end

535
  def reopenable?
536
    closed? && !source_project_missing? && source_branch_exists?
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
537 538
  end

539 540
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
541 542
  end

543
  def create_merge_request_diff
544
    fetch_ref!
545

546 547 548 549 550
    # 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
551 552 553 554 555 556
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

557 558 559 560
  def viewable_diffs
    @viewable_diffs ||= merge_request_diffs.viewable.to_a
  end

561
  def merge_request_diff_for(diff_refs_or_sha)
562 563 564 565 566 567 568 569 570 571
    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
572

573 574 575
    viewable_diffs.find do |diff|
      diff.attributes.slice(*matcher.keys) == matcher
    end
576 577
  end

578 579 580 581 582 583 584 585 586 587 588
  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

589 590 591 592 593 594 595
  def clear_memoized_shas
    @target_branch_sha = @source_branch_sha = nil

    clear_memoization(:source_branch_head)
    clear_memoization(:target_branch_head)
  end

596
  def reload_diff_if_branch_changed
597 598
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
599
      reload_diff
600 601 602
    end
  end

603
  def reload_diff(current_user = nil)
604 605
    return unless open?

606
    old_diff_refs = self.diff_refs
607 608 609
    new_diff = create_merge_request_diff

    MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
610

611 612
    new_diff_refs = self.diff_refs

613
    update_diff_discussion_positions(
614
      old_diff_refs: old_diff_refs,
615 616
      new_diff_refs: new_diff_refs,
      current_user: current_user
617
    )
618 619
  end

620
  def check_if_can_be_merged
621
    return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
622

623
    can_be_merged =
624
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
625 626

    if can_be_merged
627 628 629 630
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
631 632
  end

633
  def merge_event
634
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
635 636
  end

637
  def closed_event
638
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
639 640
  end

641
  def work_in_progress?
642
    self.class.work_in_progress?(title)
643 644 645
  end

  def wipless_title
646 647 648 649 650
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
651 652
  end

653 654
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
655 656 657

    check_if_can_be_merged

658
    can_be_merged? && !should_be_rebased?
659 660
  end

661
  def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
662 663 664
    return false unless open?
    return false if work_in_progress?
    return false if broken?
665
    return false unless skip_ci_check || mergeable_ci_state?
666
    return false unless skip_discussions_check || mergeable_discussions_state?
667 668

    true
669 670
  end

671 672 673 674 675 676 677 678
  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

679
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
680
    can_be_merged_by?(current_user) || self.author == current_user
681 682
  end

683
  def can_remove_source_branch?(current_user)
684
    !ProtectedBranch.protected?(source_project, source_branch) &&
685
      !source_project.root_ref?(source_branch) &&
686
      Ability.allowed?(current_user, :push_code, source_project) &&
687
      diff_head_sha == source_branch_head.try(:sha)
688 689
  end

690
  def should_remove_source_branch?
691
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
692 693 694
  end

  def force_remove_source_branch?
695
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
696 697 698 699 700 701
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

702
  def related_notes
703 704
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
705
    commit_ids = commit_shas.take(commits_for_notes_limit)
706

707 708 709
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
710
      .for_commit_id(commit_ids)
711 712 713 714 715 716 717 718 719 720

    # 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).
    union = Gitlab::SQL::Union
      .new([notes, commit_notes], remove_duplicates: false)
      .to_sql

    Note.from("(#{union}) #{Note.table_name}")
721
      .includes(:noteable)
722
  end
723

724
  alias_method :discussion_notes, :related_notes
725

726 727 728
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

729
    !discussions_to_be_resolved?
730 731
  end

732 733 734 735
  def for_fork?
    target_project != source_project
  end

736 737 738 739
  def project
    target_project
  end

740 741 742 743
  # 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.
744
  # This optimization does not apply to issues from external sources.
745
  def cache_merge_request_closes_issues!(current_user)
746
    return unless project.issues_enabled?
747

748
    transaction do
749
      self.merge_requests_closing_issues.delete_all
750

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

754
        self.merge_requests_closing_issues.create!(issue: issue)
755 756 757 758
      end
    end
  end

759
  # Return the set of issues that will be closed if this merge request is accepted.
760
  def closes_issues(current_user = self.author)
761
    if target_branch == project.default_branch
762
      messages = [title, description]
763
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
764

765 766
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
767 768 769 770 771
    else
      []
    end
  end

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

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

778
    ext.issues - closes_issues(current_user)
779 780
  end

781 782
  def target_project_path
    if target_project
783
      target_project.full_path
784 785 786 787 788 789 790
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
791
      source_project.full_path
792 793 794 795 796
    else
      "(removed)"
    end
  end

797 798
  def source_project_namespace
    if source_project && source_project.namespace
799
      source_project.namespace.full_path
800 801 802 803 804
    else
      "(removed)"
    end
  end

805 806
  def target_project_namespace
    if target_project && target_project.namespace
807
      target_project.namespace.full_path
808 809 810 811 812
    else
      "(removed)"
    end
  end

813 814 815
  def source_branch_exists?
    return false unless self.source_project

816
    self.source_project.repository.branch_exists?(self.source_branch)
817 818 819 820 821
  end

  def target_branch_exists?
    return false unless self.target_project

822
    self.target_project.repository.branch_exists?(self.target_branch)
823 824
  end

825
  def merge_commit_message(include_description: false)
826 827 828 829
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

830 831 832 833
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
834

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

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

842
    message.join("\n\n")
843
  end
844

845 846
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
847

848
    self.merge_when_pipeline_succeeds = false
849
    self.merge_user = nil
850 851 852 853
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
854 855 856 857

    self.save
  end

858
  # Return array of possible target branches
Steven Burgart's avatar
Steven Burgart committed
859
  # depends on target project of MR
860 861 862 863 864 865 866 867 868
  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
869
  # depends on source project of MR
870 871 872 873 874 875 876
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
877

878
  def has_ci?
879
    return false if has_no_commits?
880

881
    !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
882 883 884 885 886
  end

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

888
  def broken?
889
    has_no_commits? || branch_missing? || cannot_be_merged?
890 891
  end

892
  def can_be_merged_by?(user)
893
    access = ::Gitlab::UserAccess.new(user, project: project)
894
    access.can_update_branch?(target_branch)
895 896 897 898 899
  end

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

902
  def mergeable_ci_state?
903
    return true unless project.only_allow_merge_if_pipeline_succeeds?
904
    return true unless head_pipeline
905

906
    actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
907 908
  end

Douwe Maan's avatar
Douwe Maan committed
909
  def environments_for(current_user)
910
    return [] unless diff_head_commit
911

Douwe Maan's avatar
Douwe Maan committed
912 913 914
    @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
915

Douwe Maan's avatar
Douwe Maan committed
916 917 918 919
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
920

Douwe Maan's avatar
Douwe Maan committed
921
      h[current_user] = envs.uniq
922
    end
Douwe Maan's avatar
Douwe Maan committed
923 924

    @environments[current_user]
925 926
  end

927 928 929 930 931 932 933 934 935
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
936

937 938
  def state_icon_name
    if merged?
939
      "git-merge"
940
    elsif closed?
941
      "close"
942
    else
943
      "issue-open-m"
944 945 946
    end
  end

947 948
  def fetch_ref!
    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
949 950
  end

951
  def ref_path
952
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
953 954
  end

955 956 957 958 959
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
960
      unlock_mr
961 962
    end
  end
963

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

967
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
968
      cache = {
969 970
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
971 972 973 974 975 976 977 978 979
        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
980
    return 0 unless source_branch_sha && target_branch_sha
981

982 983
    target_project.repository
      .count_commits_between(source_branch_sha, target_branch_sha)
984
  end
985
  private :compute_diverged_commits_count
986 987 988 989 990

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

991
  def all_pipelines
992
    return Ci::Pipeline.none unless source_project
993

994
    @all_pipelines ||= source_project.pipelines
995
      .where(sha: all_commit_shas, ref: source_branch)
996
      .order(id: :desc)
997
  end
998

999
  def all_commits
1000
    # MySQL doesn't support LIMIT in a subquery.
1001 1002 1003 1004 1005
    diffs_relation = if Gitlab::Database.postgresql?
                       merge_request_diffs.recent
                     else
                       merge_request_diffs
                     end
1006

1007 1008 1009
    MergeRequestDiffCommit
      .where(merge_request_diff: diffs_relation)
      .limit(10_000)
1010 1011 1012 1013 1014 1015 1016
  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?
1017

1018 1019
      all_commits.pluck(:sha).uniq
    end
1020 1021
  end

1022 1023 1024 1025
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

1026 1027 1028 1029
  def short_merge_commit_sha
    Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
  end

1030
  def can_be_reverted?(current_user)
1031 1032 1033 1034 1035 1036
    return false unless merge_commit

    merged_at = metrics&.merged_at
    notes_association = notes_with_associations

    if merged_at
1037 1038 1039 1040 1041 1042
      # 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

1043
      notes_association = notes_association.where('created_at >= ?', cutoff)
1044 1045 1046
    end

    !merge_commit.has_been_reverted?(current_user, notes_association)
1047
  end
1048 1049

  def can_be_cherry_picked?
Fatih Acet's avatar
Fatih Acet committed
1050
    merge_commit.present?
1051
  end
1052

1053
  def has_complete_diff_refs?
1054
    diff_refs && diff_refs.complete?
1055 1056
  end

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

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

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

1068
    service = Discussions::UpdateDiffPositionService.new(
1069
      self.project,
1070
      current_user,
1071 1072 1073 1074 1075
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

1076 1077
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
1078
    end
1079 1080 1081 1082 1083 1084

    if project.resolve_outdated_diff_discussions?
      MergeRequests::ResolvedDiscussionNotificationService
        .new(project, current_user)
        .execute(self)
    end
1085 1086
  end

1087 1088 1089
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
1090

1091
  def has_commits?
1092
    merge_request_diff && commits_count > 0
1093 1094 1095 1096 1097
  end

  def has_no_commits?
    !has_commits?
  end
1098

1099
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
1100 1101 1102 1103 1104
    return false unless can_be_merged_by?(current_user)

    return true if autocomplete_precheck

    return false unless mergeable?(skip_ci_check: true)
1105
    return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
1106 1107 1108 1109
    return false if last_diff_sha != diff_head_sha

    true
  end
1110

1111 1112 1113 1114
  def update_project_counter_caches
    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
  end

micael.bergeron's avatar
micael.bergeron committed
1115
  def first_contribution?
1116
    return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
micael.bergeron's avatar
micael.bergeron committed
1117

1118 1119
    project.merge_requests.merged.where(author_id: author_id).empty?
  end
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137

  def allow_maintainer_to_push
    maintainer_push_possible? && super
  end

  alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push

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

  def can_allow_maintainer_to_push?(user)
    maintainer_push_possible? &&
      Ability.allowed?(user, :push_code, source_project)
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1138
end