merge_request.rb 27.7 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1
class MergeRequest < ActiveRecord::Base
2
  include InternalId
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

12 13
  ignore_column :locked_at,
                :ref_fetched
14

15 16
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
Z
Zeger-Jan van de Weg 已提交
17
  belongs_to :merge_user, class_name: "User"
18

19
  has_many :merge_request_diffs
20

21
  has_one :merge_request_diff,
22
    -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
23

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
  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

42 43
  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"

44
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
45

46 47 48
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
49

50 51
  belongs_to :assignee, class_name: "User"

52
  serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
Z
Zeger-Jan van de Weg 已提交
53

54 55
  after_create :ensure_merge_request_diff, unless: :importing?
  after_update :reload_diff_if_branch_changed
56

D
Dmitriy Zaporozhets 已提交
57 58 59 60
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

D
Dmitriy Zaporozhets 已提交
61 62
  # Temporary fields to store compare vars
  # when creating new merge request
63
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
D
Dmitriy Zaporozhets 已提交
64

A
Andrew8xx8 已提交
65
  state_machine :state, initial: :opened do
A
Andrew8xx8 已提交
66
    event :close do
67
      transition [:opened] => :closed
A
Andrew8xx8 已提交
68 69
    end

70
    event :mark_as_merged do
71
      transition [:opened, :locked] => :merged
A
Andrew8xx8 已提交
72 73 74
    end

    event :reopen do
75
      transition closed: :opened
A
Andrew8xx8 已提交
76 77
    end

78
    event :lock_mr do
79
      transition [:opened] => :locked
D
Dmitriy Zaporozhets 已提交
80 81
    end

82
    event :unlock_mr do
83
      transition locked: :opened
D
Dmitriy Zaporozhets 已提交
84 85
    end

A
Andrew8xx8 已提交
86 87 88
    state :opened
    state :closed
    state :merged
D
Dmitriy Zaporozhets 已提交
89
    state :locked
A
Andrew8xx8 已提交
90 91
  end

92 93 94 95 96 97
  state_machine :merge_status, initial: :unchecked do
    event :mark_as_unchecked do
      transition [:can_be_merged, :cannot_be_merged] => :unchecked
    end

    event :mark_as_mergeable do
98
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
99 100 101
    end

    event :mark_as_unmergeable do
102
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
103 104
    end

105
    state :unchecked
106 107
    state :can_be_merged
    state :cannot_be_merged
108 109

    around_transition do |merge_request, transition, block|
110
      Gitlab::Timeless.timeless(merge_request, &block)
111
    end
112
  end
113

114
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
A
Andrey Kumanyaev 已提交
115
  validates :source_branch, presence: true
I
Izaak Alpert 已提交
116
  validates :target_project, presence: true
A
Andrey Kumanyaev 已提交
117
  validates :target_branch, presence: true
J
James Lopez 已提交
118
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
119 120
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
121
  validate :validate_target_project, on: :create
D
Dmitriy Zaporozhets 已提交
122

123 124 125
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
126
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
127
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
128
  scope :from_project, ->(project) { where(source_project_id: project.id) }
129 130
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
S
Scott Le 已提交
131
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
132

133 134
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
135 136 137 138 139
  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
140

141 142
  after_save :keep_around_commit

143 144
  acts_as_paranoid

145 146 147 148
  def self.reference_prefix
    '!'
  end

149 150 151
  # 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
152
  def actual_head_pipeline
153
    head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
154 155
  end

156 157 158 159
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
160
    @reference_pattern ||= %r{
161
      (#{Project.reference_pattern})?
162 163 164 165
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

166
  def self.link_reference_pattern
167
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
168 169
  end

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

174 175 176 177
  def self.project_foreign_key
    'target_project_id'
  end

178 179 180 181 182 183 184 185 186 187 188
  # 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)
M
mhasbini 已提交
189 190 191 192
    # 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)
193 194
    union  = Gitlab::SQL::Union.new([source, target])

195
    where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
196 197
  end

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
  # 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

T
Thomas Balthazar 已提交
214 215 216 217 218 219 220 221 222 223 224 225 226 227
  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

228 229 230 231 232 233
  # 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

234
  def hook_attrs
235
    Gitlab::HookData::MergeRequestBuilder.new(self).build
236 237
  end

238 239 240 241 242 243 244 245
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

246
  # These method are needed for compatibility with issues to not mess view and other code
247 248 249 250
  def assignees
    Array(assignee)
  end

251 252 253 254 255 256 257 258
  def assignee_ids
    Array(assignee_id)
  end

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

259 260 261 262
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

263
  # `from` argument can be a Namespace or Project.
264
  def to_reference(from = nil, full: false)
265 266
    reference = "#{self.class.reference_prefix}#{iid}"

267
    "#{project.to_reference(from, full: full)}#{reference}"
268 269
  end

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
  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
294
      compare_commits.to_a.reverse.map(&:sha)
295
    else
296
      Array(diff_head_sha)
297 298 299
    end
  end

300 301 302
  # Calls `MergeWorker` to proceed with the merge process and
  # updates `merge_jid` with the MergeWorker#jid.
  # This helps tracking enqueued and ongoing merge jobs.
303
  def merge_async(user_id, params)
304 305 306 307
    jid = MergeWorker.perform_async(id, user_id, params)
    update_column(:merge_jid, jid)
  end

308 309
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
310
  end
311

312
  def raw_diffs(*args)
313
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
S
Sean McGivern 已提交
314 315
  end

316
  def diffs(diff_options = {})
317
    if compare
318
      # When saving MR diffs, `expanded` is implicitly added (because we need
319 320
      # to save the entire contents to the DB), so add that here for
      # consistency.
321
      compare.diffs(diff_options.merge(expanded: true))
322
    else
323
      merge_request_diff.diffs(diff_options)
324
    end
S
Sean McGivern 已提交
325 326
  end

J
Jacob Vosmaer 已提交
327
  def diff_size
328 329
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
330
    merge_request_diff&.real_size || diffs.real_size
J
Jacob Vosmaer 已提交
331 332
  end

333
  def diff_base_commit
334
    if persisted?
335
      merge_request_diff.base_commit
336 337
    else
      branch_merge_base_commit
338 339 340 341 342 343 344 345
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
346 347 348
    end
  end

349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  def diff_head_commit
    if persisted?
      merge_request_diff.head_commit
    else
      source_branch_head
    end
  end

  def diff_start_sha
    diff_start_commit.try(:sha)
  end

  def diff_base_sha
    diff_base_commit.try(:sha)
  end

  def diff_head_sha
    diff_head_commit.try(:sha)
  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

375 376 377 378 379 380 381 382 383 384 385 386 387 388
  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

389
  def source_branch_head
390 391
    return unless source_project

S
Sean McGivern 已提交
392
    source_project.repository.commit(source_branch_ref) if source_branch_ref
393 394 395
  end

  def target_branch_head
396
    target_project.repository.commit(target_branch_ref)
397 398
  end

399 400 401 402 403 404 405 406 407
  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

408
  def target_branch_sha
409
    @target_branch_sha || target_branch_head.try(:sha)
410 411 412
  end

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

416
  def diff_refs
417
    if persisted?
418
      merge_request_diff.diff_refs
419
    else
420 421 422 423 424
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
425
    end
426 427
  end

428 429 430 431
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

432
  def validate_branches
433
    if target_project == source_project && target_branch == source_branch
I
Izaak Alpert 已提交
434
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
435
    end
436

437
    if opened?
438
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
439 440
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
J
jubianchi 已提交
441
        errors.add :validate_branches,
G
Gabriel Mazetto 已提交
442
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
443
      end
444
    end
445 446
  end

447 448 449 450 451 452
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

453
  def validate_fork
454
    return true unless target_project && source_project
455
    return true if target_project == source_project
456
    return true unless source_project_missing?
457

458
    errors.add :validate_fork,
459
               'Source project is not a fork of the target project'
460 461
  end

462
  def merge_ongoing?
463 464
    # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
    # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
465 466 467
    return true if locked?

    !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
468 469
  end

470
  def closed_without_fork?
471
    closed? && source_project_missing?
472 473
  end

474
  def source_project_missing?
475 476 477
    return false unless for_fork?
    return true unless source_project

478
    !source_project.in_fork_network_of?(target_project)
479 480
  end

481
  def reopenable?
482
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
483 484
  end

485 486
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
487 488
  end

489
  def create_merge_request_diff
490
    fetch_ref!
491

492 493 494 495 496
    # 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
497 498 499 500 501 502
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

503 504
  def merge_request_diff_for(diff_refs_or_sha)
    @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
505
      diffs = merge_request_diffs.viewable
506 507 508 509 510 511
      h[diff_refs_or_sha] =
        if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
          diffs.find_by_diff_refs(diff_refs_or_sha)
        else
          diffs.find_by(head_commit_sha: diff_refs_or_sha)
        end
D
Douwe Maan 已提交
512 513
    end

514
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
515 516
  end

517 518 519 520 521 522 523 524 525 526 527
  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

528
  def reload_diff_if_branch_changed
529 530
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
531
      reload_diff
D
Dmitriy Zaporozhets 已提交
532 533 534
    end
  end

535
  def reload_diff(current_user = nil)
536 537
    return unless open?

538
    old_diff_refs = self.diff_refs
539

540
    create_merge_request_diff
541
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
542 543
    new_diff_refs = self.diff_refs

544
    update_diff_discussion_positions(
545
      old_diff_refs: old_diff_refs,
546 547
      new_diff_refs: new_diff_refs,
      current_user: current_user
548
    )
549 550
  end

551
  def check_if_can_be_merged
T
Toon Claes 已提交
552
    return unless unchecked? && Gitlab::Database.read_write?
553

554
    can_be_merged =
555
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
556 557

    if can_be_merged
558 559 560 561
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
562 563
  end

D
Dmitriy Zaporozhets 已提交
564
  def merge_event
565
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
566 567
  end

568
  def closed_event
569
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
570 571
  end

572
  def work_in_progress?
T
Thomas Balthazar 已提交
573
    self.class.work_in_progress?(title)
574 575 576
  end

  def wipless_title
T
Thomas Balthazar 已提交
577 578 579 580 581
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
582 583
  end

584 585
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
586 587 588 589

    check_if_can_be_merged

    can_be_merged?
590 591
  end

592
  def mergeable_state?(skip_ci_check: false)
593 594 595
    return false unless open?
    return false if work_in_progress?
    return false if broken?
596
    return false unless skip_ci_check || mergeable_ci_state?
597
    return false unless mergeable_discussions_state?
598 599

    true
600 601
  end

602 603 604 605 606 607 608 609
  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

J
James Lopez 已提交
610
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
611
    can_be_merged_by?(current_user) || self.author == current_user
612 613
  end

614
  def can_remove_source_branch?(current_user)
615
    !ProtectedBranch.protected?(source_project, source_branch) &&
616
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
617
      Ability.allowed?(current_user, :push_code, source_project) &&
618
      diff_head_commit == source_branch_head
619 620
  end

621
  def should_remove_source_branch?
622
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
623 624 625
  end

  def force_remove_source_branch?
626
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
627 628 629 630 631 632
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

633
  def related_notes
634 635
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
636
    commit_ids = commit_shas.take(commits_for_notes_limit)
637

638 639 640
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
641
      .for_commit_id(commit_ids)
642 643 644 645 646 647 648 649 650 651

    # 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}")
652
      .includes(:noteable)
653
  end
654

655
  alias_method :discussion_notes, :related_notes
656

657 658 659
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

660
    !discussions_to_be_resolved?
661 662
  end

I
Izaak Alpert 已提交
663 664 665 666
  def for_fork?
    target_project != source_project
  end

667 668 669 670
  def project
    target_project
  end

671 672 673 674
  # 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.
675
  # This optimization does not apply to issues from external sources.
676
  def cache_merge_request_closes_issues!(current_user)
677
    return unless project.issues_enabled?
678

679
    transaction do
680
      self.merge_requests_closing_issues.delete_all
681

682
      closes_issues(current_user).each do |issue|
683 684
        next if issue.is_a?(ExternalIssue)

685
        self.merge_requests_closing_issues.create!(issue: issue)
686 687 688 689
      end
    end
  end

690
  # Return the set of issues that will be closed if this merge request is accepted.
691
  def closes_issues(current_user = self.author)
692
    if target_branch == project.default_branch
693
      messages = [title, description]
694
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
695

696 697
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
698 699 700 701 702
    else
      []
    end
  end

703
  def issues_mentioned_but_not_closing(current_user)
704
    return [] unless target_branch == project.default_branch
705

706
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
707
    ext.analyze("#{title}\n#{description}")
708

709
    ext.issues - closes_issues(current_user)
710 711
  end

712 713
  def target_project_path
    if target_project
714
      target_project.full_path
715 716 717 718 719 720 721
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
722
      source_project.full_path
723 724 725 726 727
    else
      "(removed)"
    end
  end

728 729
  def source_project_namespace
    if source_project && source_project.namespace
730
      source_project.namespace.full_path
731 732 733 734 735
    else
      "(removed)"
    end
  end

736 737
  def target_project_namespace
    if target_project && target_project.namespace
738
      target_project.namespace.full_path
739 740 741 742 743
    else
      "(removed)"
    end
  end

744 745 746
  def source_branch_exists?
    return false unless self.source_project

747
    self.source_project.repository.branch_exists?(self.source_branch)
748 749 750 751 752
  end

  def target_branch_exists?
    return false unless self.target_project

753
    self.target_project.repository.branch_exists?(self.target_branch)
754 755
  end

756
  def merge_commit_message(include_description: false)
757 758 759 760
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

761 762 763 764
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
765

766
    if !include_description && closes_issues_references.present?
767
      message << "Closes #{closes_issues_references.to_sentence}"
768
    end
769
    message << "#{description}" if include_description && description.present?
770
    message << "See merge request #{to_reference(full: true)}"
771

772
    message.join("\n\n")
773
  end
774

J
James Lopez 已提交
775 776
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
777

J
James Lopez 已提交
778
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
779
    self.merge_user = nil
780 781 782 783
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
Z
Zeger-Jan van de Weg 已提交
784 785 786 787

    self.save
  end

788
  # Return array of possible target branches
S
Steven Burgart 已提交
789
  # depends on target project of MR
790 791 792 793 794 795 796 797 798
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
799
  # depends on source project of MR
800 801 802 803 804 805 806
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
807

808
  def has_ci?
809
    return false if has_no_commits?
810

811
    !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
812 813 814 815 816
  end

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

818
  def broken?
819
    has_no_commits? || branch_missing? || cannot_be_merged?
820 821
  end

822
  def can_be_merged_by?(user)
823 824 825 826 827 828 829
    access = ::Gitlab::UserAccess.new(user, project: project)
    access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch)
  end

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

832
  def mergeable_ci_state?
J
James Lopez 已提交
833
    return true unless project.only_allow_merge_if_pipeline_succeeds?
834
    return true unless head_pipeline
835

836
    actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
837 838
  end

D
Douwe Maan 已提交
839
  def environments_for(current_user)
840
    return [] unless diff_head_commit
841

D
Douwe Maan 已提交
842 843 844
    @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
845

D
Douwe Maan 已提交
846 847 848 849
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
850

D
Douwe Maan 已提交
851
      h[current_user] = envs.uniq
852
    end
D
Douwe Maan 已提交
853 854

    @environments[current_user]
855 856
  end

857 858 859 860 861 862 863 864 865
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
866

867 868 869 870 871 872 873 874 875 876
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

877 878
  def fetch_ref!
    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
879 880
  end

881
  def ref_path
882
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
883 884
  end

885 886 887 888 889
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
890
      unlock_mr
891 892
    end
  end
893

894 895 896
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

897
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
898
      cache = {
899 900
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
901 902 903 904 905 906 907 908 909
        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
910
    return 0 unless source_branch_sha && target_branch_sha
911

L
Lin Jen-Shin 已提交
912 913
    target_project.repository
      .count_commits_between(source_branch_sha, target_branch_sha)
914
  end
915
  private :compute_diverged_commits_count
916 917 918 919 920

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

921
  def all_pipelines
922
    return Ci::Pipeline.none unless source_project
923

924
    @all_pipelines ||= source_project.pipelines
925
      .where(sha: all_commit_shas, ref: source_branch)
926
      .order(id: :desc)
927
  end
928

929
  def all_commits
930
    # MySQL doesn't support LIMIT in a subquery.
M
micael.bergeron 已提交
931 932 933 934 935
    diffs_relation = if Gitlab::Database.postgresql?
                       merge_request_diffs.recent
                     else
                       merge_request_diffs
                     end
936

937 938 939
    MergeRequestDiffCommit
      .where(merge_request_diff: diffs_relation)
      .limit(10_000)
940 941 942 943 944 945 946
  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?
M
micael.bergeron 已提交
947

948 949
      all_commits.pluck(:sha).uniq
    end
950 951
  end

952 953 954 955
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

956
  def can_be_reverted?(current_user)
957
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
958
  end
959 960

  def can_be_cherry_picked?
F
Fatih Acet 已提交
961
    merge_commit.present?
962
  end
963

964
  def has_complete_diff_refs?
965
    diff_refs && diff_refs.complete?
966 967
  end

968
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
969
    return unless has_complete_diff_refs?
970 971
    return if new_diff_refs == old_diff_refs

972 973
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
974
    end
975
    return if active_diff_discussions.empty?
976

977
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
978

979
    service = Discussions::UpdateDiffPositionService.new(
980
      self.project,
981
      current_user,
982 983 984 985 986
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

987 988
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
989
    end
990 991 992 993 994 995

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

998 999 1000
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
1001

1002
  def has_commits?
1003
    merge_request_diff && commits_count > 0
1004 1005 1006 1007 1008
  end

  def has_no_commits?
    !has_commits?
  end
1009

1010
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
1011 1012 1013 1014 1015
    return false unless can_be_merged_by?(current_user)

    return true if autocomplete_precheck

    return false unless mergeable?(skip_ci_check: true)
1016
    return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
1017 1018 1019 1020
    return false if last_diff_sha != diff_head_sha

    true
  end
1021

1022 1023 1024 1025
  def update_project_counter_caches
    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
  end

M
micael.bergeron 已提交
1026
  def first_contribution?
M
micael.bergeron 已提交
1027
    return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
M
micael.bergeron 已提交
1028

M
micael.bergeron 已提交
1029 1030
    project.merge_requests.merged.where(author_id: author_id).empty?
  end
D
Dmitriy Zaporozhets 已提交
1031
end