merge_request.rb 27.5 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

11 12
  ignore_column :locked_at,
                :ref_fetched
13

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

18
  has_many :merge_request_diffs
19

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

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

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

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

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

49 50
  belongs_to :assignee, class_name: "User"

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

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

D
Dmitriy Zaporozhets 已提交
56 57 58 59
  # 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 已提交
60 61
  # Temporary fields to store compare vars
  # when creating new merge request
62
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
D
Dmitriy Zaporozhets 已提交
63

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

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

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

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

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

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

91 92 93 94 95 96
  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
97
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
98 99 100
    end

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

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

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

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

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

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

140 141
  after_save :keep_around_commit

142 143
  acts_as_paranoid

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

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

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

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

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

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

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

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

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
  # 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 已提交
213 214 215 216 217 218 219 220 221 222 223 224 225 226
  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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

348 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
  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

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

388
  def source_branch_head
389 390
    return unless source_project

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

484 485
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
486 487
  end

488
  def create_merge_request_diff
489
    fetch_ref!
490

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

  def reload_merge_request_diff
    merge_request_diff(true)
  end

502 503
  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|
504
      diffs = merge_request_diffs.viewable
505 506 507 508 509 510
      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 已提交
511 512
    end

513
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
514 515
  end

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

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

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

537
    old_diff_refs = self.diff_refs
538

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

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

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

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

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

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

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

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

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

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

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

    check_if_can_be_merged

    can_be_merged?
589 590
  end

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

    true
599 600
  end

601 602 603 604 605 606 607 608
  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 已提交
609
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
610
    can_be_merged_by?(current_user) || self.author == current_user
611 612
  end

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

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

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

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

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

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

    # 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}")
651
  end
652

653
  alias_method :discussion_notes, :related_notes
654

655 656 657
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

658
    !discussions_to_be_resolved?
659 660
  end

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

665 666 667 668
  def project
    target_project
  end

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

677
    transaction do
678
      self.merge_requests_closing_issues.delete_all
679

680
      closes_issues(current_user).each do |issue|
681 682
        next if issue.is_a?(ExternalIssue)

683
        self.merge_requests_closing_issues.create!(issue: issue)
684 685 686 687
      end
    end
  end

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

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

701
  def issues_mentioned_but_not_closing(current_user)
702
    return [] unless target_branch == project.default_branch
703

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

707
    ext.issues - closes_issues(current_user)
708 709
  end

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

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

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

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

742 743 744
  def source_branch_exists?
    return false unless self.source_project

745
    self.source_project.repository.branch_exists?(self.source_branch)
746 747 748 749 750
  end

  def target_branch_exists?
    return false unless self.target_project

751
    self.target_project.repository.branch_exists?(self.target_branch)
752 753
  end

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

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

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

770
    message.join("\n\n")
771
  end
772

J
James Lopez 已提交
773 774
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
775

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

    self.save
  end

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

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

806
  def has_ci?
807
    return false if has_no_commits?
808

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

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

816
  def broken?
817
    has_no_commits? || branch_missing? || cannot_be_merged?
818 819
  end

820
  def can_be_merged_by?(user)
821 822 823 824 825 826 827
    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)
828 829
  end

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

834
    current_head_pipeline&.success? || current_head_pipeline&.skipped?
835 836
  end

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

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

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

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

    @environments[current_user]
853 854
  end

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

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

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

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

883 884 885 886 887
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
888
      unlock_mr
889 890
    end
  end
891

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

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

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

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

919
  def all_pipelines
920
    return Ci::Pipeline.none unless source_project
921

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

927
  # Note that this could also return SHA from now dangling commits
928
  #
929
  def all_commit_shas
930
    return commit_shas unless persisted?
931

932
    diffs_relation = merge_request_diffs
933

934 935
    # MySQL doesn't support LIMIT in a subquery.
    diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
936

937 938 939 940 941
    MergeRequestDiffCommit
      .where(merge_request_diff: diffs_relation)
      .limit(10_000)
      .pluck('sha')
      .uniq
942 943
  end

944 945 946 947
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

948
  def can_be_reverted?(current_user)
949
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
950
  end
951 952

  def can_be_cherry_picked?
F
Fatih Acet 已提交
953
    merge_commit.present?
954
  end
955

956
  def has_complete_diff_refs?
957
    diff_refs && diff_refs.complete?
958 959
  end

960
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
961
    return unless has_complete_diff_refs?
962 963
    return if new_diff_refs == old_diff_refs

964 965
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
966
    end
967
    return if active_diff_discussions.empty?
968

969
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
970

971
    service = Discussions::UpdateDiffPositionService.new(
972
      self.project,
973
      current_user,
974 975 976 977 978
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

979 980
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
981
    end
982 983 984 985 986 987

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

990 991 992
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
993

994
  def has_commits?
995
    merge_request_diff && commits_count > 0
996 997 998 999 1000
  end

  def has_no_commits?
    !has_commits?
  end
1001

1002
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
1003 1004 1005 1006 1007
    return false unless can_be_merged_by?(current_user)

    return true if autocomplete_precheck

    return false unless mergeable?(skip_ci_check: true)
1008
    return false if current_head_pipeline && !(current_head_pipeline.success? || current_head_pipeline.active?)
1009 1010 1011 1012
    return false if last_diff_sha != diff_head_sha

    true
  end
1013

1014 1015 1016 1017
  def update_project_counter_caches
    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
  end

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

M
micael.bergeron 已提交
1021 1022
    project.merge_requests.merged.where(author_id: author_id).empty?
  end
D
Dmitriy Zaporozhets 已提交
1023
end