merge_request.rb 27.2 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
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
152
    @reference_pattern ||= %r{
153
      (#{Project.reference_pattern})?
154 155 156 157
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

158
  def self.link_reference_pattern
159
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
160 161
  end

162 163 164 165
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

166 167 168 169
  def self.project_foreign_key
    'target_project_id'
  end

170 171 172 173 174 175 176 177 178 179 180
  # 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 已提交
181 182 183 184
    # 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)
185 186
    union  = Gitlab::SQL::Union.new([source, target])

187
    where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
188 189
  end

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
  # 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 已提交
206 207 208 209 210 211 212 213 214 215 216 217 218 219
  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

220 221 222 223 224 225
  # 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

226
  def hook_attrs
227
    Gitlab::HookData::MergeRequestBuilder.new(self).build
228 229
  end

230 231 232 233 234 235 236 237
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

238
  # These method are needed for compatibility with issues to not mess view and other code
239 240 241 242
  def assignees
    Array(assignee)
  end

243 244 245 246 247 248 249 250
  def assignee_ids
    Array(assignee_id)
  end

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

251 252 253 254
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

255
  # `from` argument can be a Namespace or Project.
256
  def to_reference(from = nil, full: false)
257 258
    reference = "#{self.class.reference_prefix}#{iid}"

259
    "#{project.to_reference(from, full: full)}#{reference}"
260 261
  end

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
  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
      compare_commits.reverse.map(&:sha)
    else
      []
    end
  end

292 293 294
  # Calls `MergeWorker` to proceed with the merge process and
  # updates `merge_jid` with the MergeWorker#jid.
  # This helps tracking enqueued and ongoing merge jobs.
295
  def merge_async(user_id, params)
296 297 298 299
    jid = MergeWorker.perform_async(id, user_id, params)
    update_column(:merge_jid, jid)
  end

300 301
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
302
  end
303

304
  def raw_diffs(*args)
305
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
S
Sean McGivern 已提交
306 307
  end

308
  def diffs(diff_options = {})
309
    if compare
310
      # When saving MR diffs, `expanded` is implicitly added (because we need
311 312
      # to save the entire contents to the DB), so add that here for
      # consistency.
313
      compare.diffs(diff_options.merge(expanded: true))
314
    else
315
      merge_request_diff.diffs(diff_options)
316
    end
S
Sean McGivern 已提交
317 318
  end

J
Jacob Vosmaer 已提交
319
  def diff_size
320 321
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
322
    merge_request_diff&.real_size || diffs.real_size
J
Jacob Vosmaer 已提交
323 324
  end

325
  def diff_base_commit
326
    if persisted?
327
      merge_request_diff.base_commit
328 329
    else
      branch_merge_base_commit
330 331 332 333 334 335 336 337
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
338 339 340
    end
  end

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
  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

  def source_branch_head
368 369
    return unless source_project

370
    source_branch_ref = @source_branch_sha || source_branch
S
Sean McGivern 已提交
371
    source_project.repository.commit(source_branch_ref) if source_branch_ref
372 373 374 375
  end

  def target_branch_head
    target_branch_ref = @target_branch_sha || target_branch
S
Sean McGivern 已提交
376
    target_project.repository.commit(target_branch_ref) if target_branch_ref
377 378
  end

379 380 381 382 383 384 385 386 387
  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

388
  def target_branch_sha
389
    @target_branch_sha || target_branch_head.try(:sha)
390 391 392
  end

  def source_branch_sha
393
    @source_branch_sha || source_branch_head.try(:sha)
394 395
  end

396
  def diff_refs
397
    if persisted?
398
      merge_request_diff.diff_refs
399
    else
400 401 402 403 404
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
405
    end
406 407
  end

408 409 410 411
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

412
  def validate_branches
413
    if target_project == source_project && target_branch == source_branch
I
Izaak Alpert 已提交
414
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
415
    end
416

417
    if opened?
418
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
419 420
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
J
jubianchi 已提交
421
        errors.add :validate_branches,
G
Gabriel Mazetto 已提交
422
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
423
      end
424
    end
425 426
  end

427 428 429 430 431 432
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

433
  def validate_fork
434
    return true unless target_project && source_project
435
    return true if target_project == source_project
436
    return true unless source_project_missing?
437

438
    errors.add :validate_fork,
439
               'Source project is not a fork of the target project'
440 441
  end

442
  def merge_ongoing?
443 444
    # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
    # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
445 446 447
    return true if locked?

    !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
448 449
  end

450
  def closed_without_fork?
451
    closed? && source_project_missing?
452 453
  end

454
  def source_project_missing?
455 456 457
    return false unless for_fork?
    return true unless source_project

458
    !source_project.in_fork_network_of?(target_project)
459 460
  end

461
  def reopenable?
462
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
463 464
  end

465 466
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
467 468
  end

469
  def create_merge_request_diff
470
    fetch_ref!
471

472 473 474 475 476
    # 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
477 478 479 480 481 482
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

483 484 485 486 487 488 489 490 491
  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|
      diffs = merge_request_diffs.viewable.select_without_diff
      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 已提交
492 493
    end

494
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
495 496
  end

497 498 499 500 501 502 503 504 505 506 507
  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

508
  def reload_diff_if_branch_changed
509 510
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
511
      reload_diff
D
Dmitriy Zaporozhets 已提交
512 513 514
    end
  end

515
  def reload_diff(current_user = nil)
516 517
    return unless open?

518
    old_diff_refs = self.diff_refs
519

520
    create_merge_request_diff
521
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
522 523
    new_diff_refs = self.diff_refs

524
    update_diff_discussion_positions(
525
      old_diff_refs: old_diff_refs,
526 527
      new_diff_refs: new_diff_refs,
      current_user: current_user
528
    )
529 530
  end

531
  def check_if_can_be_merged
T
Toon Claes 已提交
532
    return unless unchecked? && Gitlab::Database.read_write?
533

534
    can_be_merged =
535
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
536 537

    if can_be_merged
538 539 540 541
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
542 543
  end

D
Dmitriy Zaporozhets 已提交
544
  def merge_event
545
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
546 547
  end

548
  def closed_event
549
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
550 551
  end

552
  def work_in_progress?
T
Thomas Balthazar 已提交
553
    self.class.work_in_progress?(title)
554 555 556
  end

  def wipless_title
T
Thomas Balthazar 已提交
557 558 559 560 561
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
562 563
  end

564 565
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
566 567 568 569

    check_if_can_be_merged

    can_be_merged?
570 571
  end

572
  def mergeable_state?(skip_ci_check: false)
573 574 575
    return false unless open?
    return false if work_in_progress?
    return false if broken?
576
    return false unless skip_ci_check || mergeable_ci_state?
577
    return false unless mergeable_discussions_state?
578 579

    true
580 581
  end

582 583 584 585 586 587 588 589
  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 已提交
590
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
591
    can_be_merged_by?(current_user) || self.author == current_user
592 593
  end

594
  def can_remove_source_branch?(current_user)
595
    !ProtectedBranch.protected?(source_project, source_branch) &&
596
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
597
      Ability.allowed?(current_user, :push_code, source_project) &&
598
      diff_head_commit == source_branch_head
599 600
  end

601
  def should_remove_source_branch?
602
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
603 604 605
  end

  def force_remove_source_branch?
606
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
607 608 609 610 611 612
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

613
  def related_notes
614 615
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
616
    commit_ids = commit_shas.take(commits_for_notes_limit)
617

618 619 620
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
621
      .for_commit_id(commit_ids)
622 623 624 625 626 627 628 629 630 631

    # 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}")
632
  end
633

634
  alias_method :discussion_notes, :related_notes
635

636 637 638
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

639
    !discussions_to_be_resolved?
640 641
  end

I
Izaak Alpert 已提交
642 643 644 645
  def for_fork?
    target_project != source_project
  end

646 647 648 649
  def project
    target_project
  end

650 651 652 653
  # 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.
654
  # This optimization does not apply to issues from external sources.
655
  def cache_merge_request_closes_issues!(current_user)
656
    return unless project.issues_enabled?
657

658
    transaction do
659
      self.merge_requests_closing_issues.delete_all
660

661
      closes_issues(current_user).each do |issue|
662 663
        next if issue.is_a?(ExternalIssue)

664
        self.merge_requests_closing_issues.create!(issue: issue)
665 666 667 668
      end
    end
  end

669
  # Return the set of issues that will be closed if this merge request is accepted.
670
  def closes_issues(current_user = self.author)
671
    if target_branch == project.default_branch
672
      messages = [title, description]
673
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
674

675 676
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
677 678 679 680 681
    else
      []
    end
  end

682
  def issues_mentioned_but_not_closing(current_user)
683
    return [] unless target_branch == project.default_branch
684

685
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
686
    ext.analyze("#{title}\n#{description}")
687

688
    ext.issues - closes_issues(current_user)
689 690
  end

691 692
  def target_project_path
    if target_project
693
      target_project.full_path
694 695 696 697 698 699 700
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
701
      source_project.full_path
702 703 704 705 706
    else
      "(removed)"
    end
  end

707 708
  def source_project_namespace
    if source_project && source_project.namespace
709
      source_project.namespace.full_path
710 711 712 713 714
    else
      "(removed)"
    end
  end

715 716
  def target_project_namespace
    if target_project && target_project.namespace
717
      target_project.namespace.full_path
718 719 720 721 722
    else
      "(removed)"
    end
  end

723 724 725
  def source_branch_exists?
    return false unless self.source_project

726
    self.source_project.repository.branch_exists?(self.source_branch)
727 728 729 730 731
  end

  def target_branch_exists?
    return false unless self.target_project

732
    self.target_project.repository.branch_exists?(self.target_branch)
733 734
  end

735
  def merge_commit_message(include_description: false)
736 737 738 739
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

740 741 742 743
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
744

745
    if !include_description && closes_issues_references.present?
746
      message << "Closes #{closes_issues_references.to_sentence}"
747
    end
748
    message << "#{description}" if include_description && description.present?
749
    message << "See merge request #{to_reference(full: true)}"
750

751
    message.join("\n\n")
752
  end
753

J
James Lopez 已提交
754 755
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
756

J
James Lopez 已提交
757
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
758
    self.merge_user = nil
759 760 761 762
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
Z
Zeger-Jan van de Weg 已提交
763 764 765 766

    self.save
  end

767
  # Return array of possible target branches
S
Steven Burgart 已提交
768
  # depends on target project of MR
769 770 771 772 773 774 775 776 777
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
778
  # depends on source project of MR
779 780 781 782 783 784 785
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
786

787
  def has_ci?
788
    return false if has_no_commits?
789

790
    !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
791 792 793 794 795
  end

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

797
  def broken?
798
    has_no_commits? || branch_missing? || cannot_be_merged?
799 800
  end

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

811
  def mergeable_ci_state?
J
James Lopez 已提交
812
    return true unless project.only_allow_merge_if_pipeline_succeeds?
813

814
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
815 816
  end

D
Douwe Maan 已提交
817
  def environments_for(current_user)
818
    return [] unless diff_head_commit
819

D
Douwe Maan 已提交
820 821 822
    @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
823

D
Douwe Maan 已提交
824 825 826 827
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
828

D
Douwe Maan 已提交
829
      h[current_user] = envs.uniq
830
    end
D
Douwe Maan 已提交
831 832

    @environments[current_user]
833 834
  end

835 836 837 838 839 840 841 842 843
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
844

845 846 847 848 849 850 851 852 853 854
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

855 856
  def fetch_ref!
    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
857 858
  end

859
  def ref_path
860
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
861 862
  end

863 864 865 866 867
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
868
      unlock_mr
869 870
    end
  end
871

872 873 874
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

875
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
876
      cache = {
877 878
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
879 880 881 882 883 884 885 886 887
        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
888
    return 0 unless source_branch_sha && target_branch_sha
889

890
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
891
  end
892
  private :compute_diverged_commits_count
893 894 895 896 897

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

898
  def all_pipelines
899
    return Ci::Pipeline.none unless source_project
900

901
    @all_pipelines ||= source_project.pipelines
902
      .where(sha: all_commit_shas, ref: source_branch)
903
      .order(id: :desc)
904
  end
905

906
  # Note that this could also return SHA from now dangling commits
907
  #
908
  def all_commit_shas
909
    if persisted?
910 911 912 913 914 915 916 917 918 919 920 921 922
      # MySQL doesn't support LIMIT in a subquery.
      diffs_relation =
        if Gitlab::Database.postgresql?
          merge_request_diffs.order(id: :desc).limit(100)
        else
          merge_request_diffs
        end

      column_shas = MergeRequestDiffCommit
                      .where(merge_request_diff: diffs_relation)
                      .limit(10_000)
                      .pluck('sha')

923 924 925
      serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)

      (column_shas + serialised_shas).uniq
926 927
    elsif compare_commits
      compare_commits.to_a.reverse.map(&:id)
928
    else
929
      [diff_head_sha]
930
    end
931 932
  end

933 934 935 936
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

937
  def can_be_reverted?(current_user)
938
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
939
  end
940 941

  def can_be_cherry_picked?
F
Fatih Acet 已提交
942
    merge_commit.present?
943
  end
944

945
  def has_complete_diff_refs?
946
    diff_refs && diff_refs.complete?
947 948
  end

949
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
950
    return unless has_complete_diff_refs?
951 952
    return if new_diff_refs == old_diff_refs

953 954
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
955
    end
956
    return if active_diff_discussions.empty?
957

958
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
959

960
    service = Discussions::UpdateDiffPositionService.new(
961
      self.project,
962
      current_user,
963 964 965 966 967
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

968 969
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
970
    end
971 972 973 974 975 976

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

979 980 981
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
982

983
  def has_commits?
984
    merge_request_diff && commits_count > 0
985 986 987 988 989
  end

  def has_no_commits?
    !has_commits?
  end
990

991
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
992 993 994 995 996 997 998 999 1000 1001
    return false unless can_be_merged_by?(current_user)

    return true if autocomplete_precheck

    return false unless mergeable?(skip_ci_check: true)
    return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
    return false if last_diff_sha != diff_head_sha

    true
  end
1002

1003 1004 1005 1006
  def update_project_counter_caches
    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
  end

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

M
micael.bergeron 已提交
1010 1011
    project.merge_requests.merged.where(author_id: author_id).empty?
  end
D
Dmitriy Zaporozhets 已提交
1012
end