merge_request.rb 24.3 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
class MergeRequest < ActiveRecord::Base
2
  include InternalId
3
  include Issuable
4
  include Noteable
5
  include Referable
6
  include Sortable
7
  include IgnorableColumn
James Lopez's avatar
James Lopez committed
8
  include CreatedAtFilterable
9 10

  ignore_column :position
11

12 13
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
14
  belongs_to :merge_user, class_name: "User"
15

16
  has_many :merge_request_diffs
17 18 19
  has_one :merge_request_diff,
    -> { order('merge_request_diffs.id DESC') }

20 21
  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"

22
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
23

24 25 26
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
27

28 29
  belongs_to :assignee, class_name: "User"

30
  serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
31

32 33
  after_create :ensure_merge_request_diff, unless: :importing?
  after_update :reload_diff_if_branch_changed
34

35
  delegate :commits, :real_size, :commit_shas, :commits_count,
36
    to: :merge_request_diff, prefix: nil
37

38 39 40 41
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

42 43
  # Temporary fields to store compare vars
  # when creating new merge request
44
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
45

Andrew8xx8's avatar
Andrew8xx8 committed
46
  state_machine :state, initial: :opened do
47 48 49 50
    event :close do
      transition [:reopened, :opened] => :closed
    end

51
    event :mark_as_merged do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
52
      transition [:reopened, :opened, :locked] => :merged
53 54 55
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
56
      transition closed: :reopened
57 58
    end

59
    event :lock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
60 61 62
      transition [:reopened, :opened] => :locked
    end

63
    event :unlock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
64 65 66
      transition locked: :reopened
    end

67 68 69 70 71
    after_transition any => :locked do |merge_request, transition|
      merge_request.locked_at = Time.now
      merge_request.save
    end

72
    after_transition locked: (any - :locked) do |merge_request, transition|
73 74 75 76
      merge_request.locked_at = nil
      merge_request.save
    end

77 78 79 80
    state :opened
    state :reopened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
81
    state :locked
82 83
  end

84 85 86 87 88 89
  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
90
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
91 92 93
    end

    event :mark_as_unmergeable do
94
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
95 96
    end

97
    state :unchecked
98 99
    state :can_be_merged
    state :cannot_be_merged
100 101

    around_transition do |merge_request, transition, block|
102
      Gitlab::Timeless.timeless(merge_request, &block)
103
    end
104
  end
105

106
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
107
  validates :source_branch, presence: true
108
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
109
  validates :target_branch, presence: true
110
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
111 112
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
113
  validate :validate_target_project, on: :create
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
114

115 116 117
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
118
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
119
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
120
  scope :from_project, ->(project) { where(source_project_id: project.id) }
121 122
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
123
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
124

125 126
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
127 128 129 130 131
  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
132

133 134
  after_save :keep_around_commit

135 136 137 138
  def self.reference_prefix
    '!'
  end

139 140 141 142
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
143
    @reference_pattern ||= %r{
144
      (#{Project.reference_pattern})?
145 146 147 148
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

149
  def self.link_reference_pattern
150
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
151 152
  end

153 154 155 156
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

157 158 159 160
  def self.project_foreign_key
    'target_project_id'
  end

161 162 163 164 165 166 167 168 169 170 171
  # 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)
172 173 174 175
    # 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)
176 177 178 179 180
    union  = Gitlab::SQL::Union.new([source, target])

    where("merge_requests.id IN (#{union.to_sql})")
  end

181 182 183 184 185 186 187 188 189 190 191 192 193 194
  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

195 196 197 198 199 200 201 202
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

203
  # These method are needed for compatibility with issues to not mess view and other code
204 205 206 207
  def assignees
    Array(assignee)
  end

208 209 210 211 212 213 214 215
  def assignee_ids
    Array(assignee_id)
  end

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

216 217 218 219
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

220
  # `from` argument can be a Namespace or Project.
221
  def to_reference(from = nil, full: false)
222 223
    reference = "#{self.class.reference_prefix}#{iid}"

224
    "#{project.to_reference(from, full: full)}#{reference}"
225 226
  end

227 228
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
229
  end
230

231
  def raw_diffs(*args)
232
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
233 234
  end

235
  def diffs(diff_options = {})
236
    if compare
237
      # When saving MR diffs, `expanded` is implicitly added (because we need
238 239
      # to save the entire contents to the DB), so add that here for
      # consistency.
240
      compare.diffs(diff_options.merge(expanded: true))
241
    else
242
      merge_request_diff.diffs(diff_options)
243
    end
244 245
  end

246
  def diff_size
247 248 249
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
    return real_size if merge_request_diff
250

251
    diffs.real_size
252 253
  end

254
  def diff_base_commit
255
    if persisted?
256
      merge_request_diff.base_commit
257 258
    else
      branch_merge_base_commit
259 260 261 262 263 264 265 266
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
267 268 269
    end
  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 294 295 296
  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
297 298
    return unless source_project

299
    source_branch_ref = @source_branch_sha || source_branch
300
    source_project.repository.commit(source_branch_ref) if source_branch_ref
301 302 303 304
  end

  def target_branch_head
    target_branch_ref = @target_branch_sha || target_branch
305
    target_project.repository.commit(target_branch_ref) if target_branch_ref
306 307
  end

308 309 310 311 312 313 314 315 316
  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

317
  def target_branch_sha
318
    @target_branch_sha || target_branch_head.try(:sha)
319 320 321
  end

  def source_branch_sha
322
    @source_branch_sha || source_branch_head.try(:sha)
323 324
  end

325
  def diff_refs
326
    if persisted?
327
      merge_request_diff.diff_refs
328
    else
329 330 331 332 333
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
334
    end
335 336
  end

337 338 339 340
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

341
  def validate_branches
342
    if target_project == source_project && target_branch == source_branch
343
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
344
    end
345

346
    if opened? || reopened?
347
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
348 349
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
350
        errors.add :validate_branches,
Gabriel Mazetto's avatar
Gabriel Mazetto committed
351
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
352
      end
353
    end
354 355
  end

356 357 358 359 360 361
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

362
  def validate_fork
363
    return true unless target_project && source_project
364
    return true if target_project == source_project
365
    return true unless source_project_missing?
366

367
    errors.add :validate_fork,
368
               'Source project is not a fork of the target project'
369 370 371
  end

  def closed_without_fork?
372
    closed? && source_project_missing?
373 374
  end

375
  def source_project_missing?
376 377 378 379
    return false unless for_fork?
    return true unless source_project

    !source_project.forked_from?(target_project)
380 381
  end

382
  def reopenable?
383
    closed? && !source_project_missing? && source_branch_exists?
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
384 385
  end

386 387
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
388 389
  end

390 391 392 393 394 395 396 397 398
  def create_merge_request_diff
    merge_request_diffs.create
    reload_merge_request_diff
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

399 400 401 402 403 404 405 406 407
  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
Douwe Maan's avatar
Douwe Maan committed
408 409
    end

410
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
411 412
  end

413 414 415 416 417 418 419 420 421 422 423
  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

424
  def reload_diff_if_branch_changed
425
    if source_branch_changed? || target_branch_changed?
426
      reload_diff
427 428 429
    end
  end

430
  def reload_diff(current_user = nil)
431 432
    return unless open?

433
    old_diff_refs = self.diff_refs
434
    create_merge_request_diff
435
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
436 437
    new_diff_refs = self.diff_refs

438
    update_diff_discussion_positions(
439
      old_diff_refs: old_diff_refs,
440 441
      new_diff_refs: new_diff_refs,
      current_user: current_user
442
    )
443 444
  end

445
  def check_if_can_be_merged
446 447
    return unless unchecked?

448
    can_be_merged =
449
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
450 451

    if can_be_merged
452 453 454 455
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
456 457
  end

458
  def merge_event
459
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
460 461
  end

462
  def closed_event
463
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
464 465
  end

466
  def work_in_progress?
467
    self.class.work_in_progress?(title)
468 469 470
  end

  def wipless_title
471 472 473 474 475
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
476 477
  end

478 479
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
480 481 482 483

    check_if_can_be_merged

    can_be_merged?
484 485
  end

486
  def mergeable_state?(skip_ci_check: false)
487 488 489
    return false unless open?
    return false if work_in_progress?
    return false if broken?
490
    return false unless skip_ci_check || mergeable_ci_state?
491
    return false unless mergeable_discussions_state?
492 493

    true
494 495
  end

496
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
497
    can_be_merged_by?(current_user) || self.author == current_user
498 499
  end

500
  def can_remove_source_branch?(current_user)
501
    !ProtectedBranch.protected?(source_project, source_branch) &&
502
      !source_project.root_ref?(source_branch) &&
503
      Ability.allowed?(current_user, :push_code, source_project) &&
504
      diff_head_commit == source_branch_head
505 506
  end

507
  def should_remove_source_branch?
508
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
509 510 511
  end

  def force_remove_source_branch?
512
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
513 514 515 516 517 518
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

519
  def related_notes
520 521
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
522
    commit_ids = commit_shas.take(commits_for_notes_limit)
523

524 525
    Note.where(
      "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
526
      "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
527
      mr_id: id,
528 529 530
      commit_ids: commit_ids,
      target_project_id: target_project_id,
      source_project_id: source_project_id
531
    )
532
  end
533

534
  alias_method :discussion_notes, :related_notes
535

536 537 538
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

539
    !discussions_to_be_resolved?
540 541
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
542 543
  def hook_attrs
    attrs = {
544
      source: source_project.try(:hook_attrs),
Kirill Zaitsev's avatar
Kirill Zaitsev committed
545
      target: target_project.hook_attrs,
546
      last_commit: nil,
547 548 549 550
      work_in_progress: work_in_progress?,
      total_time_spent: total_time_spent,
      human_total_time_spent: human_total_time_spent,
      human_time_estimate: human_time_estimate
Kirill Zaitsev's avatar
Kirill Zaitsev committed
551 552
    }

553
    if diff_head_commit
554
      attrs[:last_commit] = diff_head_commit.hook_attrs
Kirill Zaitsev's avatar
Kirill Zaitsev committed
555 556 557 558 559
    end

    attributes.merge!(attrs)
  end

560 561 562 563
  def for_fork?
    target_project != source_project
  end

564 565 566 567
  def project
    target_project
  end

568 569 570 571
  # 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.
572
  # This optimization does not apply to issues from external sources.
573
  def cache_merge_request_closes_issues!(current_user)
574 575
    return if project.has_external_issue_tracker?

576
    transaction do
577
      self.merge_requests_closing_issues.delete_all
578

579
      closes_issues(current_user).each do |issue|
580
        self.merge_requests_closing_issues.create!(issue: issue)
581 582 583 584
      end
    end
  end

585
  # Return the set of issues that will be closed if this merge request is accepted.
586
  def closes_issues(current_user = self.author)
587
    if target_branch == project.default_branch
588
      messages = [title, description]
589
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
590

591 592
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
593 594 595 596 597
    else
      []
    end
  end

598
  def issues_mentioned_but_not_closing(current_user)
599
    return [] unless target_branch == project.default_branch
600

601
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
602
    ext.analyze("#{title}\n#{description}")
603

604
    ext.issues - closes_issues(current_user)
605 606
  end

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
  def target_project_path
    if target_project
      target_project.path_with_namespace
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
      source_project.path_with_namespace
    else
      "(removed)"
    end
  end

623 624
  def source_project_namespace
    if source_project && source_project.namespace
625
      source_project.namespace.full_path
626 627 628 629 630
    else
      "(removed)"
    end
  end

631 632
  def target_project_namespace
    if target_project && target_project.namespace
633
      target_project.namespace.full_path
634 635 636 637 638
    else
      "(removed)"
    end
  end

639 640 641 642 643 644 645 646 647 648 649 650
  def source_branch_exists?
    return false unless self.source_project

    self.source_project.repository.branch_names.include?(self.source_branch)
  end

  def target_branch_exists?
    return false unless self.target_project

    self.target_project.repository.branch_names.include?(self.target_branch)
  end

651
  def merge_commit_message(include_description: false)
652 653 654 655
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

656 657 658 659
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
660

661
    if !include_description && closes_issues_references.present?
662
      message << "Closes #{closes_issues_references.to_sentence}"
663 664
    end

665
    message << "#{description}" if include_description && description.present?
666 667
    message << "See merge request #{to_reference}"

668
    message.join("\n\n")
669
  end
670

671 672
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
673

674
    self.merge_when_pipeline_succeeds = false
675
    self.merge_user = nil
676 677 678 679
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
680 681 682 683

    self.save
  end

684
  # Return array of possible target branches
Steven Burgart's avatar
Steven Burgart committed
685
  # depends on target project of MR
686 687 688 689 690 691 692 693 694
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
Steven Burgart's avatar
Steven Burgart committed
695
  # depends on source project of MR
696 697 698 699 700 701 702
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
703 704

  def locked_long_ago?
Ben Bodenmiller's avatar
Ben Bodenmiller committed
705 706 707
    return false unless locked?

    locked_at.nil? || locked_at < (Time.now - 1.day)
708
  end
709 710

  def has_ci?
711 712 713 714
    has_ci_integration = source_project.try(:ci_service)
    uses_gitlab_ci = all_pipelines.any?

    (has_ci_integration || uses_gitlab_ci) && commits.any?
715 716 717 718 719
  end

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

721
  def broken?
722
    has_no_commits? || branch_missing? || cannot_be_merged?
723 724
  end

725
  def can_be_merged_by?(user)
726 727 728 729 730 731 732
    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)
733 734
  end

735
  def mergeable_ci_state?
736
    return true unless project.only_allow_merge_if_pipeline_succeeds?
737

738
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
739 740
  end

Douwe Maan's avatar
Douwe Maan committed
741
  def environments_for(current_user)
742
    return [] unless diff_head_commit
743

Douwe Maan's avatar
Douwe Maan committed
744 745 746
    @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
747

Douwe Maan's avatar
Douwe Maan committed
748 749 750 751
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
752

Douwe Maan's avatar
Douwe Maan committed
753
      h[current_user] = envs.uniq
754
    end
Douwe Maan's avatar
Douwe Maan committed
755 756

    @environments[current_user]
757 758
  end

759 760 761 762 763 764 765 766 767
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
768

769 770 771 772 773 774 775 776 777 778
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

779 780 781 782
  def fetch_ref
    target_project.repository.fetch_ref(
      source_project.repository.path_to_repo,
      "refs/heads/#{source_branch}",
783
      ref_path
784
    )
785
    update_column(:ref_fetched, true)
786 787
  end

788 789 790 791
  def ref_path
    "refs/merge-requests/#{iid}/head"
  end

792
  def ref_fetched?
793 794 795 796 797 798 799
    super ||
      begin
        computed_value = project.repository.ref_exists?(ref_path)
        update_column(:ref_fetched, true) if computed_value

        computed_value
      end
800 801 802
  end

  def ensure_ref_fetched
803
    fetch_ref unless ref_fetched?
804 805
  end

806 807 808 809 810 811 812 813
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
      unlock_mr if locked?
    end
  end
814

815 816 817
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

818
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
819
      cache = {
820 821
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
822 823 824 825 826 827 828 829 830
        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
831
    return 0 unless source_branch_sha && target_branch_sha
832

833
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
834
  end
835
  private :compute_diverged_commits_count
836 837 838 839 840

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

841
  def all_pipelines
842
    return Ci::Pipeline.none unless source_project
843

844
    @all_pipelines ||= source_project.pipelines
845
      .where(sha: all_commit_shas, ref: source_branch)
846
      .order(id: :desc)
847
  end
848

849
  # Note that this could also return SHA from now dangling commits
850
  #
851
  def all_commit_shas
852
    if persisted?
853
      merge_request_diffs.preload(:merge_request_diff_commits).flat_map(&:commit_shas).uniq
854 855
    elsif compare_commits
      compare_commits.to_a.reverse.map(&:id)
856
    else
857
      [diff_head_sha]
858
    end
859 860
  end

861 862 863 864
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

865
  def can_be_reverted?(current_user)
866
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
867
  end
868 869

  def can_be_cherry_picked?
Fatih Acet's avatar
Fatih Acet committed
870
    merge_commit.present?
871
  end
872

873
  def has_complete_diff_refs?
874
    diff_refs && diff_refs.complete?
875 876
  end

877
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
878
    return unless has_complete_diff_refs?
879 880
    return if new_diff_refs == old_diff_refs

881 882
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
883
    end
884
    return if active_diff_discussions.empty?
885

886
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
887

888
    service = Discussions::UpdateDiffPositionService.new(
889
      self.project,
890
      current_user,
891 892 893 894 895
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

896 897
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
898 899 900
    end
  end

901 902 903
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
904

905
  def has_commits?
906
    merge_request_diff && commits_count > 0
907 908 909 910 911
  end

  def has_no_commits?
    !has_commits?
  end
912

913
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
914 915 916 917 918 919 920 921 922 923
    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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
924
end