merge_request.rb 23.9 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 Sortable
7 8 9
  include IgnorableColumn

  ignore_column :position
10

11 12
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
Z
Zeger-Jan van de Weg 已提交
13
  belongs_to :merge_user, class_name: "User"
14

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

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

21 22
  has_many :events, as: :target, dependent: :destroy

23
  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
24

25 26
  belongs_to :assignee, class_name: "User"

27
  serialize :merge_params, Hash # rubocop:disable Cop/ActiverecordSerialize
Z
Zeger-Jan van de Weg 已提交
28

29 30
  after_create :ensure_merge_request_diff, unless: :importing?
  after_update :reload_diff_if_branch_changed
31

32 33
  delegate :commits, :real_size, :commits_sha, :commits_count,
    to: :merge_request_diff, prefix: nil
I
Izaak Alpert 已提交
34

D
Dmitriy Zaporozhets 已提交
35 36 37 38
  # 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 已提交
39 40
  # Temporary fields to store compare vars
  # when creating new merge request
41
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
D
Dmitriy Zaporozhets 已提交
42

A
Andrew8xx8 已提交
43
  state_machine :state, initial: :opened do
A
Andrew8xx8 已提交
44 45 46 47
    event :close do
      transition [:reopened, :opened] => :closed
    end

48
    event :mark_as_merged do
D
Dmitriy Zaporozhets 已提交
49
      transition [:reopened, :opened, :locked] => :merged
A
Andrew8xx8 已提交
50 51 52
    end

    event :reopen do
A
Andrew8xx8 已提交
53
      transition closed: :reopened
A
Andrew8xx8 已提交
54 55
    end

56
    event :lock_mr do
D
Dmitriy Zaporozhets 已提交
57 58 59
      transition [:reopened, :opened] => :locked
    end

60
    event :unlock_mr do
D
Dmitriy Zaporozhets 已提交
61 62 63
      transition locked: :reopened
    end

64 65 66 67 68
    after_transition any => :locked do |merge_request, transition|
      merge_request.locked_at = Time.now
      merge_request.save
    end

69
    after_transition locked: (any - :locked) do |merge_request, transition|
70 71 72 73
      merge_request.locked_at = nil
      merge_request.save
    end

A
Andrew8xx8 已提交
74 75 76 77
    state :opened
    state :reopened
    state :closed
    state :merged
D
Dmitriy Zaporozhets 已提交
78
    state :locked
A
Andrew8xx8 已提交
79 80
  end

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

    event :mark_as_unmergeable do
91
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
92 93
    end

94
    state :unchecked
95 96
    state :can_be_merged
    state :cannot_be_merged
97 98

    around_transition do |merge_request, transition, block|
99
      Gitlab::Timeless.timeless(merge_request, &block)
100
    end
101
  end
102

103
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
A
Andrey Kumanyaev 已提交
104
  validates :source_branch, presence: true
I
Izaak Alpert 已提交
105
  validates :target_project, presence: true
A
Andrey Kumanyaev 已提交
106
  validates :target_branch, presence: true
J
James Lopez 已提交
107
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
108 109
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
110
  validate :validate_target_project, on: :create
D
Dmitriy Zaporozhets 已提交
111

112 113 114
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
115
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
116
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
117
  scope :from_project, ->(project) { where(source_project_id: project.id) }
118 119
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
S
Scott Le 已提交
120
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
121

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

130 131
  after_save :keep_around_commit

132 133 134 135
  def self.reference_prefix
    '!'
  end

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

146
  def self.link_reference_pattern
147
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
148 149
  end

150 151 152 153
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

154 155 156 157
  def self.project_foreign_key
    'target_project_id'
  end

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

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

T
Thomas Balthazar 已提交
178 179 180 181 182 183 184 185 186 187 188 189 190 191
  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

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

  # This method is needed for compatibility with issues to not mess view and other code
  def assignees
    Array(assignee)
  end

  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

209
  # `from` argument can be a Namespace or Project.
210
  def to_reference(from = nil, full: false)
211 212
    reference = "#{self.class.reference_prefix}#{iid}"

213
    "#{project.to_reference(from, full: full)}#{reference}"
214 215
  end

216 217
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
218
  end
219

220
  def raw_diffs(*args)
221
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
S
Sean McGivern 已提交
222 223
  end

224
  def diffs(diff_options = {})
225
    if compare
226
      # When saving MR diffs, `expanded` is implicitly added (because we need
227 228
      # to save the entire contents to the DB), so add that here for
      # consistency.
229
      compare.diffs(diff_options.merge(expanded: true))
230
    else
231
      merge_request_diff.diffs(diff_options)
232
    end
S
Sean McGivern 已提交
233 234
  end

J
Jacob Vosmaer 已提交
235
  def diff_size
236 237 238
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
    return real_size if merge_request_diff
239

240
    diffs.real_size
J
Jacob Vosmaer 已提交
241 242
  end

243
  def diff_base_commit
244
    if persisted?
245
      merge_request_diff.base_commit
246 247
    else
      branch_merge_base_commit
248 249 250 251 252 253 254 255
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
256 257 258
    end
  end

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
  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
286 287
    return unless source_project

288
    source_branch_ref = @source_branch_sha || source_branch
S
Sean McGivern 已提交
289
    source_project.repository.commit(source_branch_ref) if source_branch_ref
290 291 292 293
  end

  def target_branch_head
    target_branch_ref = @target_branch_sha || target_branch
S
Sean McGivern 已提交
294
    target_project.repository.commit(target_branch_ref) if target_branch_ref
295 296
  end

297 298 299 300 301 302 303 304 305
  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

306
  def target_branch_sha
307
    @target_branch_sha || target_branch_head.try(:sha)
308 309 310
  end

  def source_branch_sha
311
    @source_branch_sha || source_branch_head.try(:sha)
312 313
  end

314
  def diff_refs
315
    if persisted?
316
      merge_request_diff.diff_refs
317
    else
318 319 320 321 322
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
323
    end
324 325
  end

326 327 328 329
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

330
  def validate_branches
331
    if target_project == source_project && target_branch == source_branch
I
Izaak Alpert 已提交
332
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
333
    end
334

335
    if opened? || reopened?
336
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
337 338
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
J
jubianchi 已提交
339
        errors.add :validate_branches,
G
Gabriel Mazetto 已提交
340
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
341
      end
342
    end
343 344
  end

345 346 347 348 349 350
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

351
  def validate_fork
352
    return true unless target_project && source_project
353
    return true if target_project == source_project
354
    return true unless source_project_missing?
355

356
    errors.add :validate_fork,
357
               'Source project is not a fork of the target project'
358 359 360
  end

  def closed_without_fork?
361
    closed? && source_project_missing?
362 363
  end

364
  def source_project_missing?
365 366 367 368
    return false unless for_fork?
    return true unless source_project

    !source_project.forked_from?(target_project)
369 370
  end

371
  def reopenable?
372
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
373 374
  end

375 376
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
377 378
  end

379 380 381 382 383 384 385 386 387
  def create_merge_request_diff
    merge_request_diffs.create
    reload_merge_request_diff
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

388 389 390 391 392 393 394 395 396
  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 已提交
397 398
    end

399
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
400 401
  end

402 403 404 405 406 407 408 409 410 411 412
  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

413
  def reload_diff_if_branch_changed
D
Dmitriy Zaporozhets 已提交
414
    if source_branch_changed? || target_branch_changed?
415
      reload_diff
D
Dmitriy Zaporozhets 已提交
416 417 418
    end
  end

419
  def reload_diff(current_user = nil)
420 421
    return unless open?

422
    old_diff_refs = self.diff_refs
423
    create_merge_request_diff
424
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
425 426
    new_diff_refs = self.diff_refs

427
    update_diff_discussion_positions(
428
      old_diff_refs: old_diff_refs,
429 430
      new_diff_refs: new_diff_refs,
      current_user: current_user
431
    )
432 433
  end

434
  def check_if_can_be_merged
435 436
    return unless unchecked?

437
    can_be_merged =
438
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
439 440

    if can_be_merged
441 442 443 444
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
445 446
  end

D
Dmitriy Zaporozhets 已提交
447
  def merge_event
448
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
449 450
  end

451
  def closed_event
452
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
453 454
  end

455
  def work_in_progress?
T
Thomas Balthazar 已提交
456
    self.class.work_in_progress?(title)
457 458 459
  end

  def wipless_title
T
Thomas Balthazar 已提交
460 461 462 463 464
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
465 466
  end

467 468
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
469 470 471 472

    check_if_can_be_merged

    can_be_merged?
473 474
  end

475
  def mergeable_state?(skip_ci_check: false)
476 477 478
    return false unless open?
    return false if work_in_progress?
    return false if broken?
479
    return false unless skip_ci_check || mergeable_ci_state?
480
    return false unless mergeable_discussions_state?
481 482

    true
483 484
  end

J
James Lopez 已提交
485
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
486
    can_be_merged_by?(current_user) || self.author == current_user
487 488
  end

489
  def can_remove_source_branch?(current_user)
490
    !ProtectedBranch.protected?(source_project, source_branch) &&
491
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
492
      Ability.allowed?(current_user, :push_code, source_project) &&
493
      diff_head_commit == source_branch_head
494 495
  end

496
  def should_remove_source_branch?
497
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
498 499 500
  end

  def force_remove_source_branch?
501
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
502 503 504 505 506 507
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

508
  def related_notes
509 510 511 512
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
    commit_ids = commits.last(commits_for_notes_limit).map(&:id)

513 514
    Note.where(
      "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
515
      "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
516
      mr_id: id,
517 518 519
      commit_ids: commit_ids,
      target_project_id: target_project_id,
      source_project_id: source_project_id
520
    )
521
  end
522

523
  alias_method :discussion_notes, :related_notes
524

525 526 527
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

528
    !discussions_to_be_resolved?
529 530
  end

K
Kirill Zaitsev 已提交
531 532
  def hook_attrs
    attrs = {
533
      source: source_project.try(:hook_attrs),
K
Kirill Zaitsev 已提交
534
      target: target_project.hook_attrs,
535
      last_commit: nil,
536 537 538 539
      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
K
Kirill Zaitsev 已提交
540 541
    }

542
    if diff_head_commit
D
Douwe Maan 已提交
543
      attrs[:last_commit] = diff_head_commit.hook_attrs
K
Kirill Zaitsev 已提交
544 545 546 547 548
    end

    attributes.merge!(attrs)
  end

I
Izaak Alpert 已提交
549 550 551 552
  def for_fork?
    target_project != source_project
  end

553 554 555 556
  def project
    target_project
  end

557 558 559 560
  # 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.
561
  # This optimization does not apply to issues from external sources.
562
  def cache_merge_request_closes_issues!(current_user)
563 564
    return if project.has_external_issue_tracker?

565
    transaction do
566
      self.merge_requests_closing_issues.delete_all
567

568
      closes_issues(current_user).each do |issue|
569
        self.merge_requests_closing_issues.create!(issue: issue)
570 571 572 573
      end
    end
  end

574
  # Return the set of issues that will be closed if this merge request is accepted.
575
  def closes_issues(current_user = self.author)
576
    if target_branch == project.default_branch
577
      messages = [title, description]
578
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
579

580 581
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
582 583 584 585 586
    else
      []
    end
  end

587
  def issues_mentioned_but_not_closing(current_user)
588
    return [] unless target_branch == project.default_branch
589

590
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
591
    ext.analyze("#{title}\n#{description}")
592

593
    ext.issues - closes_issues(current_user)
594 595
  end

596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
  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

612 613
  def source_project_namespace
    if source_project && source_project.namespace
614
      source_project.namespace.full_path
615 616 617 618 619
    else
      "(removed)"
    end
  end

620 621
  def target_project_namespace
    if target_project && target_project.namespace
622
      target_project.namespace.full_path
623 624 625 626 627
    else
      "(removed)"
    end
  end

628 629 630 631 632 633 634 635 636 637 638 639
  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

640
  def merge_commit_message(include_description: false)
641 642 643 644
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

645 646 647 648
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
649

650
    if !include_description && closes_issues_references.present?
651
      message << "Closes #{closes_issues_references.to_sentence}"
652 653
    end

654
    message << "#{description}" if include_description && description.present?
655 656
    message << "See merge request #{to_reference}"

657
    message.join("\n\n")
658
  end
659

J
James Lopez 已提交
660 661
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
662

J
James Lopez 已提交
663
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
664
    self.merge_user = nil
665 666 667 668
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
Z
Zeger-Jan van de Weg 已提交
669 670 671 672

    self.save
  end

673
  # Return array of possible target branches
S
Steven Burgart 已提交
674
  # depends on target project of MR
675 676 677 678 679 680 681 682 683
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
684
  # depends on source project of MR
685 686 687 688 689 690 691
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
692 693

  def locked_long_ago?
B
Ben Bodenmiller 已提交
694 695 696
    return false unless locked?

    locked_at.nil? || locked_at < (Time.now - 1.day)
697
  end
698 699

  def has_ci?
700 701 702 703
    has_ci_integration = source_project.try(:ci_service)
    uses_gitlab_ci = all_pipelines.any?

    (has_ci_integration || uses_gitlab_ci) && commits.any?
704 705 706 707 708
  end

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

710
  def broken?
711
    has_no_commits? || branch_missing? || cannot_be_merged?
712 713
  end

714
  def can_be_merged_by?(user)
715 716 717 718 719 720 721
    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)
722 723
  end

724
  def mergeable_ci_state?
J
James Lopez 已提交
725
    return true unless project.only_allow_merge_if_pipeline_succeeds?
726

727
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
728 729
  end

D
Douwe Maan 已提交
730
  def environments_for(current_user)
731
    return [] unless diff_head_commit
732

D
Douwe Maan 已提交
733 734 735
    @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
736

D
Douwe Maan 已提交
737 738 739 740
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
741

D
Douwe Maan 已提交
742
      h[current_user] = envs.uniq
743
    end
D
Douwe Maan 已提交
744 745

    @environments[current_user]
746 747
  end

748 749 750 751 752 753 754 755 756
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
757

758 759 760 761 762 763 764 765 766 767
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

768 769 770 771
  def fetch_ref
    target_project.repository.fetch_ref(
      source_project.repository.path_to_repo,
      "refs/heads/#{source_branch}",
772
      ref_path
773 774 775
    )
  end

776 777 778 779
  def ref_path
    "refs/merge-requests/#{iid}/head"
  end

780 781
  def ref_fetched?
    project.repository.ref_exists?(ref_path)
782 783 784
  end

  def ensure_ref_fetched
785
    fetch_ref unless ref_fetched?
786 787
  end

788 789 790 791 792 793 794 795
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
      unlock_mr if locked?
    end
  end
796

797 798 799
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

800
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
801
      cache = {
802 803
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
804 805 806 807 808 809 810 811 812
        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
813
    return 0 unless source_branch_sha && target_branch_sha
814

815
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
816
  end
817
  private :compute_diverged_commits_count
818 819 820 821 822

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

823
  def all_pipelines
824
    return Ci::Pipeline.none unless source_project
825

826
    @all_pipelines ||= source_project.pipelines
827 828
      .where(sha: all_commits_sha, ref: source_branch)
      .order(id: :desc)
829
  end
830

831
  # Note that this could also return SHA from now dangling commits
832
  #
833
  def all_commits_sha
834 835
    if persisted?
      merge_request_diffs.flat_map(&:commits_sha).uniq
836 837
    elsif compare_commits
      compare_commits.to_a.reverse.map(&:id)
838
    else
839
      [diff_head_sha]
840
    end
841 842
  end

843 844 845 846
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

847
  def can_be_reverted?(current_user)
848
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
849
  end
850 851

  def can_be_cherry_picked?
F
Fatih Acet 已提交
852
    merge_commit.present?
853
  end
854

855
  def has_complete_diff_refs?
856
    diff_refs && diff_refs.complete?
857 858
  end

859
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
860
    return unless has_complete_diff_refs?
861 862
    return if new_diff_refs == old_diff_refs

863 864
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
865
    end
866
    return if active_diff_discussions.empty?
867

868
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
869

870
    service = Discussions::UpdateDiffPositionService.new(
871
      self.project,
872
      current_user,
873 874 875 876 877
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

878 879
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
880 881 882
    end
  end

883 884 885
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
886

887
  def has_commits?
888
    merge_request_diff && commits_count > 0
889 890 891 892 893
  end

  def has_no_commits?
    !has_commits?
  end
894

895
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
896 897 898 899 900 901 902 903 904 905
    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
D
Dmitriy Zaporozhets 已提交
906
end