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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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, unless: :importing?
75
  after_update :clear_memoized_shas
76
  after_update :reload_diff_if_branch_changed
77
  after_save :ensure_metrics
78

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
102 103
    end

104
    event :unlock_mr do
105
      transition locked: :opened
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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 187 188
  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
189

190 191
  after_save :keep_around_commit

192 193 194
  alias_attribute :project, :target_project
  alias_attribute :project_id, :target_project_id

195 196 197 198
  def self.reference_prefix
    '!'
  end

199
  def rebase_in_progress?
200 201 202
    strong_memoize(:rebase_in_progress) do
      # The source project can be deleted
      next false unless source_project
203

204 205
      source_project.repository.rebase_in_progress?(id)
    end
206 207
  end

208 209 210
  # 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
211
  def actual_head_pipeline
212
    head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
213 214
  end

215 216 217 218 219 220
  def merge_pipeline
    return unless merged?

    target_project.pipeline_for(target_branch, merge_commit_sha)
  end

221 222 223 224
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
225
    @reference_pattern ||= %r{
226
      (#{Project.reference_pattern})?
227 228 229 230
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

231
  def self.link_reference_pattern
232
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
233 234
  end

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

239 240 241 242
  def self.project_foreign_key
    'target_project_id'
  end

243 244 245 246 247 248 249 250 251 252 253
  # 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)
254 255
    # unscoping unnecessary conditions that'll be applied
    # when executing `where("merge_requests.id IN (#{union.to_sql})")`
256 257
    source = unscoped.where(source_project_id: relation)
    target = unscoped.where(target_project_id: relation)
258

259
    from_union([source, target])
260 261
  end

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
  # 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

278
  WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
Thomas Balthazar's avatar
Thomas Balthazar committed
279 280 281 282 283 284 285 286 287 288 289 290 291

  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

292 293 294 295 296 297 298 299
  def committers
    @committers ||= commits.committers
  end

  def authors
    User.from_union([committers, User.where(id: self.author_id)])
  end

300 301 302 303 304 305
  # 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

306
  def hook_attrs
307
    Gitlab::HookData::MergeRequestBuilder.new(self).build
308 309
  end

310 311 312 313 314 315 316 317
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

318
  # These method are needed for compatibility with issues to not mess view and other code
319 320 321 322
  def assignees
    Array(assignee)
  end

323 324 325 326 327 328 329 330
  def assignee_ids
    Array(assignee_id)
  end

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

331 332 333 334
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

335
  # `from` argument can be a Namespace or Project.
336
  def to_reference(from = nil, full: false)
337 338
    reference = "#{self.class.reference_prefix}#{iid}"

339
    "#{project.to_reference(from, full: full)}#{reference}"
340 341
  end

342
  def commits
343 344 345 346 347 348 349 350 351
    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)
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
  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
368
      compare_commits.to_a.reverse.map(&:sha)
369
    else
370
      Array(diff_head_sha)
371 372 373
    end
  end

374 375 376 377 378 379 380 381 382
  # 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

383
  def supports_suggestion?
384
    true
385 386
  end

387 388 389
  # Calls `MergeWorker` to proceed with the merge process and
  # updates `merge_jid` with the MergeWorker#jid.
  # This helps tracking enqueued and ongoing merge jobs.
390
  def merge_async(user_id, params)
391
    jid = MergeWorker.perform_async(id, user_id, params.to_h)
392 393 394
    update_column(:merge_jid, jid)
  end

lulalala's avatar
lulalala committed
395 396 397 398 399 400 401 402 403 404
  def merge_participants
    participants = [author]

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

    participants
  end

405 406
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
407
  end
408

409
  def raw_diffs(*args)
410
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
411 412
  end

413
  def diffs(diff_options = {})
414
    if compare
415
      # When saving MR diffs, `expanded` is implicitly added (because we need
416 417
      # to save the entire contents to the DB), so add that here for
      # consistency.
418
      compare.diffs(diff_options.merge(expanded: true))
419
    else
420
      merge_request_diff.diffs(diff_options)
421
    end
422 423
  end

424 425 426 427
  def non_latest_diffs
    merge_request_diffs.where.not(id: merge_request_diff.id)
  end

428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
  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

450
  def diff_size
451 452
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
453
    merge_request_diff&.real_size || diffs.real_size
454 455
  end

456 457 458 459 460 461 462 463 464 465 466 467
  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

468
  def diff_base_commit
469
    if persisted?
470
      merge_request_diff.base_commit
471 472
    else
      branch_merge_base_commit
473 474 475 476 477 478 479 480
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
481 482 483
    end
  end

484 485 486 487 488 489 490 491 492
  def diff_head_commit
    if persisted?
      merge_request_diff.head_commit
    else
      source_branch_head
    end
  end

  def diff_start_sha
493 494 495 496 497
    if persisted?
      merge_request_diff.start_commit_sha
    else
      target_branch_head.try(:sha)
    end
498 499 500
  end

  def diff_base_sha
501 502 503 504 505
    if persisted?
      merge_request_diff.base_commit_sha
    else
      branch_merge_base_commit.try(:sha)
    end
506 507 508
  end

  def diff_head_sha
509 510 511 512 513
    if persisted?
      merge_request_diff.head_commit_sha
    else
      source_branch_head.try(:sha)
    end
514 515 516 517 518 519 520 521
  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

522 523 524 525 526 527 528 529 530 531 532 533 534 535
  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

536
  def source_branch_head
537 538 539 540 541
    strong_memoize(:source_branch_head) do
      if source_project && source_branch_ref
        source_project.repository.commit(source_branch_ref)
      end
    end
542 543 544
  end

  def target_branch_head
545 546 547
    strong_memoize(:target_branch_head) do
      target_project.repository.commit(target_branch_ref)
    end
548 549
  end

550 551 552 553 554 555 556 557 558
  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

559
  def target_branch_sha
560
    @target_branch_sha || target_branch_head.try(:sha)
561 562 563
  end

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

567
  def diff_refs
568 569 570 571 572 573 574 575 576 577 578 579 580
    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
    )
581 582
  end

583 584 585 586
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

587
  def validate_branches
588
    if target_project == source_project && target_branch == source_branch
589 590
      errors.add :branch_conflict, "You can't use same project/branch for source and target"
      return
591
    end
592

593
    if opened?
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608
      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}"
        )
609
      end
610
    end
611 612
  end

613 614 615 616 617 618
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

619
  def validate_fork
620
    return true unless target_project && source_project
621
    return true if target_project == source_project
622
    return true unless source_project_missing?
623

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

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

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

636
  def closed_without_fork?
637
    closed? && source_project_missing?
638 639
  end

640
  def source_project_missing?
641 642 643
    return false unless for_fork?
    return true unless source_project

644
    !source_project.in_fork_network_of?(target_project)
645 646
  end

647
  def reopenable?
648
    closed? && !source_project_missing? && source_branch_exists?
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
649 650
  end

651 652
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
653 654
  end

655
  def create_merge_request_diff
656
    fetch_ref!
657

658 659 660 661 662
    # 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
663 664
  end

665 666 667 668
  def viewable_diffs
    @viewable_diffs ||= merge_request_diffs.viewable.to_a
  end

669
  def merge_request_diff_for(diff_refs_or_sha)
670 671 672 673 674 675 676 677 678 679
    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
680

681 682 683
    viewable_diffs.find do |diff|
      diff.attributes.slice(*matcher.keys) == matcher
    end
684 685
  end

686 687 688 689 690 691 692 693 694 695 696
  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

697 698 699 700 701 702 703
  def clear_memoized_shas
    @target_branch_sha = @source_branch_sha = nil

    clear_memoization(:source_branch_head)
    clear_memoization(:target_branch_head)
  end

704
  def reload_diff_if_branch_changed
705 706
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
707
      reload_diff
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
708 709 710
    end
  end

711
  # rubocop: disable CodeReuse/ServiceClass
712
  def reload_diff(current_user = nil)
713 714
    return unless open?

715
    MergeRequests::ReloadDiffsService.new(self, current_user).execute
716
  end
717
  # rubocop: enable CodeReuse/ServiceClass
718

719
  def check_if_can_be_merged
720
    return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
721

722
    can_be_merged =
723
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
724 725

    if can_be_merged
726 727 728 729
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
730 731
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
732
  def merge_event
733
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
734 735
  end

736
  def closed_event
737
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
738 739
  end

740
  def work_in_progress?
Thomas Balthazar's avatar
Thomas Balthazar committed
741
    self.class.work_in_progress?(title)
742 743 744
  end

  def wipless_title
Thomas Balthazar's avatar
Thomas Balthazar committed
745 746 747 748 749
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
750 751
  end

752 753
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
754 755 756

    check_if_can_be_merged

757
    can_be_merged? && !should_be_rebased?
758 759
  end

760
  def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
761 762 763
    return false unless open?
    return false if work_in_progress?
    return false if broken?
764
    return false unless skip_ci_check || mergeable_ci_state?
765
    return false unless skip_discussions_check || mergeable_discussions_state?
766 767

    true
768 769
  end

770 771 772 773 774 775 776 777
  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

778
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
779
    can_be_merged_by?(current_user) || self.author == current_user
780 781
  end

782
  def can_remove_source_branch?(current_user)
783
    !ProtectedBranch.protected?(source_project, source_branch) &&
784
      !source_project.root_ref?(source_branch) &&
http://jneen.net/'s avatar
http://jneen.net/ committed
785
      Ability.allowed?(current_user, :push_code, source_project) &&
786
      diff_head_sha == source_branch_head.try(:sha)
787 788
  end

789
  def should_remove_source_branch?
790
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
791 792 793
  end

  def force_remove_source_branch?
794
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
795 796 797 798 799 800
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

801
  def notify_conflict?
802 803 804 805 806 807 808 809
    (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
810 811
  end

812
  def related_notes
813 814
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
815
    commit_ids = commit_shas.take(commits_for_notes_limit)
816

817 818 819
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
820
      .for_commit_id(commit_ids)
821 822 823 824 825

    # 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).
826 827
    Note
      .from_union([notes, commit_notes], remove_duplicates: false)
828
      .includes(:noteable)
829
  end
830

831
  alias_method :discussion_notes, :related_notes
832

833 834 835
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

836
    !discussions_to_be_resolved?
837 838
  end

839 840 841 842
  def for_fork?
    target_project != source_project
  end

843 844 845 846
  # 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.
847
  # This optimization does not apply to issues from external sources.
848
  def cache_merge_request_closes_issues!(current_user = self.author)
849
    return unless project.issues_enabled?
850
    return if closed? || merged?
851

852
    transaction do
853
      self.merge_requests_closing_issues.delete_all
854

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

858
        self.merge_requests_closing_issues.create!(issue: issue)
859 860 861 862
      end
    end
  end

863 864 865 866 867 868 869 870 871 872 873 874
  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

875
  # Return the set of issues that will be closed if this merge request is accepted.
876
  def closes_issues(current_user = self.author)
877
    if target_branch == project.default_branch
878
      messages = [title, description]
879
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
880

881 882
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
883 884 885 886 887
    else
      []
    end
  end

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

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

894
    ext.issues - visible_closing_issues_for(current_user)
895 896
  end

897 898
  def target_project_path
    if target_project
899
      target_project.full_path