merge_request.rb 24.5 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1
class MergeRequest < ActiveRecord::Base
2
  include InternalId
3
  include Issuable
4
  include Noteable
5
  include Referable
6
  include Sortable
7
  include IgnorableColumn
J
James Lopez 已提交
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"
Z
Zeger-Jan van de Weg 已提交
14
  belongs_to :merge_user, class_name: "User"
15

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

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
Z
Zeger-Jan van de Weg 已提交
31

32 33
  after_create :ensure_merge_request_diff, unless: :importing?
  after_update :reload_diff_if_branch_changed
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
    event :close do
45
      transition [:opened] => :closed
A
Andrew8xx8 已提交
46 47
    end

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

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

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

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

A
Andrew8xx8 已提交
64 65 66
    state :opened
    state :closed
    state :merged
D
Dmitriy Zaporozhets 已提交
67
    state :locked
A
Andrew8xx8 已提交
68 69
  end

70 71 72 73 74 75
  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
76
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
77 78 79
    end

    event :mark_as_unmergeable do
80
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
81 82
    end

83
    state :unchecked
84 85
    state :can_be_merged
    state :cannot_be_merged
86 87

    around_transition do |merge_request, transition, block|
88
      Gitlab::Timeless.timeless(merge_request, &block)
89
    end
90
  end
91

92
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
A
Andrey Kumanyaev 已提交
93
  validates :source_branch, presence: true
I
Izaak Alpert 已提交
94
  validates :target_project, presence: true
A
Andrey Kumanyaev 已提交
95
  validates :target_branch, presence: true
J
James Lopez 已提交
96
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
97 98
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
99
  validate :validate_target_project, on: :create
D
Dmitriy Zaporozhets 已提交
100

101 102 103
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
104
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
105
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
106
  scope :from_project, ->(project) { where(source_project_id: project.id) }
107 108
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
S
Scott Le 已提交
109
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
110

111 112
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
113 114 115 116 117
  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
118

119 120
  after_save :keep_around_commit

121 122 123 124
  def self.reference_prefix
    '!'
  end

125 126 127 128
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
129
    @reference_pattern ||= %r{
130
      (#{Project.reference_pattern})?
131 132 133 134
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

135
  def self.link_reference_pattern
136
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
137 138
  end

139 140 141 142
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

143 144 145 146
  def self.project_foreign_key
    'target_project_id'
  end

147 148 149 150 151 152 153 154 155 156 157
  # 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 已提交
158 159 160 161
    # 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)
162 163 164 165 166
    union  = Gitlab::SQL::Union.new([source, target])

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

T
Thomas Balthazar 已提交
167 168 169 170 171 172 173 174 175 176 177 178 179 180
  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

181 182 183 184 185 186 187 188
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

189
  # These method are needed for compatibility with issues to not mess view and other code
190 191 192 193
  def assignees
    Array(assignee)
  end

194 195 196 197 198 199 200 201
  def assignee_ids
    Array(assignee_id)
  end

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

202 203 204 205
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

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

210
    "#{project.to_reference(from, full: full)}#{reference}"
211 212
  end

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
  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

243 244
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
245
  end
246

247
  def raw_diffs(*args)
248
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
S
Sean McGivern 已提交
249 250
  end

251
  def diffs(diff_options = {})
252
    if compare
253
      # When saving MR diffs, `expanded` is implicitly added (because we need
254 255
      # to save the entire contents to the DB), so add that here for
      # consistency.
256
      compare.diffs(diff_options.merge(expanded: true))
257
    else
258
      merge_request_diff.diffs(diff_options)
259
    end
S
Sean McGivern 已提交
260 261
  end

J
Jacob Vosmaer 已提交
262
  def diff_size
263 264
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
265
    merge_request_diff&.real_size || diffs.real_size
J
Jacob Vosmaer 已提交
266 267
  end

268
  def diff_base_commit
269
    if persisted?
270
      merge_request_diff.base_commit
271 272
    else
      branch_merge_base_commit
273 274 275 276 277 278 279 280
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
281 282 283
    end
  end

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
  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
311 312
    return unless source_project

313
    source_branch_ref = @source_branch_sha || source_branch
S
Sean McGivern 已提交
314
    source_project.repository.commit(source_branch_ref) if source_branch_ref
315 316 317 318
  end

  def target_branch_head
    target_branch_ref = @target_branch_sha || target_branch
S
Sean McGivern 已提交
319
    target_project.repository.commit(target_branch_ref) if target_branch_ref
320 321
  end

322 323 324 325 326 327 328 329 330
  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

331
  def target_branch_sha
332
    @target_branch_sha || target_branch_head.try(:sha)
333 334 335
  end

  def source_branch_sha
336
    @source_branch_sha || source_branch_head.try(:sha)
337 338
  end

339
  def diff_refs
340
    if persisted?
341
      merge_request_diff.diff_refs
342
    else
343 344 345 346 347
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
348
    end
349 350
  end

351 352 353 354
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

355
  def validate_branches
356
    if target_project == source_project && target_branch == source_branch
I
Izaak Alpert 已提交
357
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
358
    end
359

360
    if opened?
361
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
362 363
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
J
jubianchi 已提交
364
        errors.add :validate_branches,
G
Gabriel Mazetto 已提交
365
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
366
      end
367
    end
368 369
  end

370 371 372 373 374 375
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

376
  def validate_fork
377
    return true unless target_project && source_project
378
    return true if target_project == source_project
379
    return true unless source_project_missing?
380

381
    errors.add :validate_fork,
382
               'Source project is not a fork of the target project'
383 384
  end

385 386 387 388 389 390
  def merge_ongoing?
    return false unless merge_jid

    Gitlab::SidekiqStatus.num_running([merge_jid]) > 0
  end

391
  def closed_without_fork?
392
    closed? && source_project_missing?
393 394
  end

395
  def source_project_missing?
396 397 398 399
    return false unless for_fork?
    return true unless source_project

    !source_project.forked_from?(target_project)
400 401
  end

402
  def reopenable?
403
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
404 405
  end

406 407
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
408 409
  end

410 411 412 413 414 415 416 417 418
  def create_merge_request_diff
    merge_request_diffs.create
    reload_merge_request_diff
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

419 420 421 422 423 424 425 426 427
  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 已提交
428 429
    end

430
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
431 432
  end

433 434 435 436 437 438 439 440 441 442 443
  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

444
  def reload_diff_if_branch_changed
D
Dmitriy Zaporozhets 已提交
445
    if source_branch_changed? || target_branch_changed?
446
      reload_diff
D
Dmitriy Zaporozhets 已提交
447 448 449
    end
  end

450
  def reload_diff(current_user = nil)
451 452
    return unless open?

453
    old_diff_refs = self.diff_refs
454
    create_merge_request_diff
455
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
456 457
    new_diff_refs = self.diff_refs

458
    update_diff_discussion_positions(
459
      old_diff_refs: old_diff_refs,
460 461
      new_diff_refs: new_diff_refs,
      current_user: current_user
462
    )
463 464
  end

465
  def check_if_can_be_merged
466 467
    return unless unchecked?

468
    can_be_merged =
469
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
470 471

    if can_be_merged
472 473 474 475
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
476 477
  end

D
Dmitriy Zaporozhets 已提交
478
  def merge_event
479
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
480 481
  end

482
  def closed_event
483
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
484 485
  end

486
  def work_in_progress?
T
Thomas Balthazar 已提交
487
    self.class.work_in_progress?(title)
488 489 490
  end

  def wipless_title
T
Thomas Balthazar 已提交
491 492 493 494 495
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
496 497
  end

498 499
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
500 501 502 503

    check_if_can_be_merged

    can_be_merged?
504 505
  end

506
  def mergeable_state?(skip_ci_check: false)
507 508 509
    return false unless open?
    return false if work_in_progress?
    return false if broken?
510
    return false unless skip_ci_check || mergeable_ci_state?
511
    return false unless mergeable_discussions_state?
512 513

    true
514 515
  end

J
James Lopez 已提交
516
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
517
    can_be_merged_by?(current_user) || self.author == current_user
518 519
  end

520
  def can_remove_source_branch?(current_user)
521
    !ProtectedBranch.protected?(source_project, source_branch) &&
522
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
523
      Ability.allowed?(current_user, :push_code, source_project) &&
524
      diff_head_commit == source_branch_head
525 526
  end

527
  def should_remove_source_branch?
528
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
529 530 531
  end

  def force_remove_source_branch?
532
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
533 534 535 536 537 538
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

539
  def related_notes
540 541
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
542
    commit_ids = commit_shas.take(commits_for_notes_limit)
543

544 545
    Note.where(
      "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
546
      "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
547
      mr_id: id,
548 549 550
      commit_ids: commit_ids,
      target_project_id: target_project_id,
      source_project_id: source_project_id
551
    )
552
  end
553

554
  alias_method :discussion_notes, :related_notes
555

556 557 558
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

559
    !discussions_to_be_resolved?
560 561
  end

K
Kirill Zaitsev 已提交
562 563
  def hook_attrs
    attrs = {
564
      source: source_project.try(:hook_attrs),
K
Kirill Zaitsev 已提交
565
      target: target_project.hook_attrs,
566
      last_commit: nil,
567 568 569 570
      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 已提交
571 572
    }

573
    if diff_head_commit
D
Douwe Maan 已提交
574
      attrs[:last_commit] = diff_head_commit.hook_attrs
K
Kirill Zaitsev 已提交
575 576 577 578 579
    end

    attributes.merge!(attrs)
  end

I
Izaak Alpert 已提交
580 581 582 583
  def for_fork?
    target_project != source_project
  end

584 585 586 587
  def project
    target_project
  end

588 589 590 591
  # 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.
592
  # This optimization does not apply to issues from external sources.
593
  def cache_merge_request_closes_issues!(current_user)
594
    return unless project.issues_enabled?
595

596
    transaction do
597
      self.merge_requests_closing_issues.delete_all
598

599
      closes_issues(current_user).each do |issue|
600
        self.merge_requests_closing_issues.create!(issue: issue)
601 602 603 604
      end
    end
  end

605
  # Return the set of issues that will be closed if this merge request is accepted.
606
  def closes_issues(current_user = self.author)
607
    if target_branch == project.default_branch
608
      messages = [title, description]
609
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
610

611 612
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
613 614 615 616 617
    else
      []
    end
  end

618
  def issues_mentioned_but_not_closing(current_user)
619
    return [] unless target_branch == project.default_branch
620

621
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
622
    ext.analyze("#{title}\n#{description}")
623

624
    ext.issues - closes_issues(current_user)
625 626
  end

627 628
  def target_project_path
    if target_project
629
      target_project.full_path
630 631 632 633 634 635 636
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
637
      source_project.full_path
638 639 640 641 642
    else
      "(removed)"
    end
  end

643 644
  def source_project_namespace
    if source_project && source_project.namespace
645
      source_project.namespace.full_path
646 647 648 649 650
    else
      "(removed)"
    end
  end

651 652
  def target_project_namespace
    if target_project && target_project.namespace
653
      target_project.namespace.full_path
654 655 656 657 658
    else
      "(removed)"
    end
  end

659 660 661 662 663 664 665 666 667 668 669 670
  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

671
  def merge_commit_message(include_description: false)
672 673 674 675
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

676 677 678 679
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
680

681
    if !include_description && closes_issues_references.present?
682
      message << "Closes #{closes_issues_references.to_sentence}"
683 684
    end

685
    message << "#{description}" if include_description && description.present?
686 687
    message << "See merge request #{to_reference}"

688
    message.join("\n\n")
689
  end
690

J
James Lopez 已提交
691 692
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
693

J
James Lopez 已提交
694
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
695
    self.merge_user = nil
696 697 698 699
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
Z
Zeger-Jan van de Weg 已提交
700 701 702 703

    self.save
  end

704
  # Return array of possible target branches
S
Steven Burgart 已提交
705
  # depends on target project of MR
706 707 708 709 710 711 712 713 714
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
715
  # depends on source project of MR
716 717 718 719 720 721 722
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
723

724
  def has_ci?
725 726 727 728
    has_ci_integration = source_project.try(:ci_service)
    uses_gitlab_ci = all_pipelines.any?

    (has_ci_integration || uses_gitlab_ci) && commits.any?
729 730 731 732 733
  end

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

735
  def broken?
736
    has_no_commits? || branch_missing? || cannot_be_merged?
737 738
  end

739
  def can_be_merged_by?(user)
740 741 742 743 744 745 746
    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)
747 748
  end

749
  def mergeable_ci_state?
J
James Lopez 已提交
750
    return true unless project.only_allow_merge_if_pipeline_succeeds?
751

752
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
753 754
  end

D
Douwe Maan 已提交
755
  def environments_for(current_user)
756
    return [] unless diff_head_commit
757

D
Douwe Maan 已提交
758 759 760
    @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
761

D
Douwe Maan 已提交
762 763 764 765
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
766

D
Douwe Maan 已提交
767
      h[current_user] = envs.uniq
768
    end
D
Douwe Maan 已提交
769 770

    @environments[current_user]
771 772
  end

773 774 775 776 777 778 779 780 781
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
782

783 784 785 786 787 788 789 790 791 792
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

793 794 795 796
  def fetch_ref
    target_project.repository.fetch_ref(
      source_project.repository.path_to_repo,
      "refs/heads/#{source_branch}",
797
      ref_path
798
    )
799
    update_column(:ref_fetched, true)
800 801
  end

802 803 804 805
  def ref_path
    "refs/merge-requests/#{iid}/head"
  end

806
  def ref_fetched?
807 808 809 810 811 812 813
    super ||
      begin
        computed_value = project.repository.ref_exists?(ref_path)
        update_column(:ref_fetched, true) if computed_value

        computed_value
      end
814 815 816
  end

  def ensure_ref_fetched
817
    fetch_ref unless ref_fetched?
818 819
  end

820 821 822 823 824 825 826 827
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
      unlock_mr if locked?
    end
  end
828

829 830 831
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

832
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
833
      cache = {
834 835
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
836 837 838 839 840 841 842 843 844
        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
845
    return 0 unless source_branch_sha && target_branch_sha
846

847
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
848
  end
849
  private :compute_diverged_commits_count
850 851 852 853 854

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

855
  def all_pipelines
856
    return Ci::Pipeline.none unless source_project
857

858
    @all_pipelines ||= source_project.pipelines
859
      .where(sha: all_commit_shas, ref: source_branch)
860
      .order(id: :desc)
861
  end
862

863
  # Note that this could also return SHA from now dangling commits
864
  #
865
  def all_commit_shas
866
    if persisted?
867 868 869 870
      column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)')
      serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)

      (column_shas + serialised_shas).uniq
871 872
    elsif compare_commits
      compare_commits.to_a.reverse.map(&:id)
873
    else
874
      [diff_head_sha]
875
    end
876 877
  end

878 879 880 881
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

882
  def can_be_reverted?(current_user)
883
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
884
  end
885 886

  def can_be_cherry_picked?
F
Fatih Acet 已提交
887
    merge_commit.present?
888
  end
889

890
  def has_complete_diff_refs?
891
    diff_refs && diff_refs.complete?
892 893
  end

894
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
895
    return unless has_complete_diff_refs?
896 897
    return if new_diff_refs == old_diff_refs

898 899
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
900
    end
901
    return if active_diff_discussions.empty?
902

903
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
904

905
    service = Discussions::UpdateDiffPositionService.new(
906
      self.project,
907
      current_user,
908 909 910 911 912
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

913 914
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
915 916 917
    end
  end

918 919 920
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
921

922
  def has_commits?
923
    merge_request_diff && commits_count > 0
924 925 926 927 928
  end

  def has_no_commits?
    !has_commits?
  end
929

930
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
931 932 933 934 935 936 937 938 939 940
    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 已提交
941
end