merge_request.rb 24.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 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
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'
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
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

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

205 206 207 208 209 210 211 212
  def assignee_ids
    Array(assignee_id)
  end

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

213 214 215 216
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

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

221
    "#{project.to_reference(from, full: full)}#{reference}"
222 223
  end

224 225
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
226
  end
227

228
  def raw_diffs(*args)
229
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
S
Sean McGivern 已提交
230 231
  end

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

J
Jacob Vosmaer 已提交
243
  def diff_size
244 245 246
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
    return real_size if merge_request_diff
247

248
    diffs.real_size
J
Jacob Vosmaer 已提交
249 250
  end

251
  def diff_base_commit
252
    if persisted?
253
      merge_request_diff.base_commit
254 255
    else
      branch_merge_base_commit
256 257 258 259 260 261 262 263
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
264 265 266
    end
  end

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 292 293
  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
294 295
    return unless source_project

296
    source_branch_ref = @source_branch_sha || source_branch
S
Sean McGivern 已提交
297
    source_project.repository.commit(source_branch_ref) if source_branch_ref
298 299 300 301
  end

  def target_branch_head
    target_branch_ref = @target_branch_sha || target_branch
S
Sean McGivern 已提交
302
    target_project.repository.commit(target_branch_ref) if target_branch_ref
303 304
  end

305 306 307 308 309 310 311 312 313
  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

314
  def target_branch_sha
315
    @target_branch_sha || target_branch_head.try(:sha)
316 317 318
  end

  def source_branch_sha
319
    @source_branch_sha || source_branch_head.try(:sha)
320 321
  end

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

334 335 336 337
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

338
  def validate_branches
339
    if target_project == source_project && target_branch == source_branch
I
Izaak Alpert 已提交
340
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
341
    end
342

343
    if opened? || reopened?
344
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
345 346
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
J
jubianchi 已提交
347
        errors.add :validate_branches,
G
Gabriel Mazetto 已提交
348
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
349
      end
350
    end
351 352
  end

353 354 355 356 357 358
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

359
  def validate_fork
360
    return true unless target_project && source_project
361
    return true if target_project == source_project
362
    return true unless source_project_missing?
363

364
    errors.add :validate_fork,
365
               'Source project is not a fork of the target project'
366 367 368
  end

  def closed_without_fork?
369
    closed? && source_project_missing?
370 371
  end

372
  def source_project_missing?
373 374 375 376
    return false unless for_fork?
    return true unless source_project

    !source_project.forked_from?(target_project)
377 378
  end

379
  def reopenable?
380
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
381 382
  end

383 384
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
385 386
  end

387 388 389 390 391 392 393 394 395
  def create_merge_request_diff
    merge_request_diffs.create
    reload_merge_request_diff
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

396 397 398 399 400 401 402 403 404
  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 已提交
405 406
    end

407
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
408 409
  end

410 411 412 413 414 415 416 417 418 419 420
  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

421
  def reload_diff_if_branch_changed
D
Dmitriy Zaporozhets 已提交
422
    if source_branch_changed? || target_branch_changed?
423
      reload_diff
D
Dmitriy Zaporozhets 已提交
424 425 426
    end
  end

427
  def reload_diff(current_user = nil)
428 429
    return unless open?

430
    old_diff_refs = self.diff_refs
431
    create_merge_request_diff
432
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
433 434
    new_diff_refs = self.diff_refs

435
    update_diff_discussion_positions(
436
      old_diff_refs: old_diff_refs,
437 438
      new_diff_refs: new_diff_refs,
      current_user: current_user
439
    )
440 441
  end

442
  def check_if_can_be_merged
443 444
    return unless unchecked?

445
    can_be_merged =
446
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
447 448

    if can_be_merged
449 450 451 452
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
453 454
  end

D
Dmitriy Zaporozhets 已提交
455
  def merge_event
456
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
457 458
  end

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

463
  def work_in_progress?
T
Thomas Balthazar 已提交
464
    self.class.work_in_progress?(title)
465 466 467
  end

  def wipless_title
T
Thomas Balthazar 已提交
468 469 470 471 472
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
473 474
  end

475 476
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
477 478 479 480

    check_if_can_be_merged

    can_be_merged?
481 482
  end

483
  def mergeable_state?(skip_ci_check: false)
484 485 486
    return false unless open?
    return false if work_in_progress?
    return false if broken?
487
    return false unless skip_ci_check || mergeable_ci_state?
488
    return false unless mergeable_discussions_state?
489 490

    true
491 492
  end

J
James Lopez 已提交
493
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
494
    can_be_merged_by?(current_user) || self.author == current_user
495 496
  end

497
  def can_remove_source_branch?(current_user)
498
    !ProtectedBranch.protected?(source_project, source_branch) &&
499
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
500
      Ability.allowed?(current_user, :push_code, source_project) &&
501
      diff_head_commit == source_branch_head
502 503
  end

504
  def should_remove_source_branch?
505
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
506 507 508
  end

  def force_remove_source_branch?
509
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
510 511 512 513 514 515
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

516
  def related_notes
517 518 519 520
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
    commit_ids = commits.last(commits_for_notes_limit).map(&:id)

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

531
  alias_method :discussion_notes, :related_notes
532

533 534 535
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

536
    !discussions_to_be_resolved?
537 538
  end

K
Kirill Zaitsev 已提交
539 540
  def hook_attrs
    attrs = {
541
      source: source_project.try(:hook_attrs),
K
Kirill Zaitsev 已提交
542
      target: target_project.hook_attrs,
543
      last_commit: nil,
544 545 546 547
      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 已提交
548 549
    }

550
    if diff_head_commit
D
Douwe Maan 已提交
551
      attrs[:last_commit] = diff_head_commit.hook_attrs
K
Kirill Zaitsev 已提交
552 553 554 555 556
    end

    attributes.merge!(attrs)
  end

I
Izaak Alpert 已提交
557 558 559 560
  def for_fork?
    target_project != source_project
  end

561 562 563 564
  def project
    target_project
  end

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

573
    transaction do
574
      self.merge_requests_closing_issues.delete_all
575

576
      closes_issues(current_user).each do |issue|
577
        self.merge_requests_closing_issues.create!(issue: issue)
578 579 580 581
      end
    end
  end

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

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

595
  def issues_mentioned_but_not_closing(current_user)
596
    return [] unless target_branch == project.default_branch
597

598
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
599
    ext.analyze("#{title}\n#{description}")
600

601
    ext.issues - closes_issues(current_user)
602 603
  end

604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
  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

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

628 629
  def target_project_namespace
    if target_project && target_project.namespace
630
      target_project.namespace.full_path
631 632 633 634 635
    else
      "(removed)"
    end
  end

636 637 638 639 640 641 642 643 644 645 646 647
  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

648
  def merge_commit_message(include_description: false)
649 650 651 652
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

653 654 655 656
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
657

658
    if !include_description && closes_issues_references.present?
659
      message << "Closes #{closes_issues_references.to_sentence}"
660 661
    end

662
    message << "#{description}" if include_description && description.present?
663 664
    message << "See merge request #{to_reference}"

665
    message.join("\n\n")
666
  end
667

J
James Lopez 已提交
668 669
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
670

J
James Lopez 已提交
671
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
672
    self.merge_user = nil
673 674 675 676
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
Z
Zeger-Jan van de Weg 已提交
677 678 679 680

    self.save
  end

681
  # Return array of possible target branches
S
Steven Burgart 已提交
682
  # depends on target project of MR
683 684 685 686 687 688 689 690 691
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
692
  # depends on source project of MR
693 694 695 696 697 698 699
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
700 701

  def locked_long_ago?
B
Ben Bodenmiller 已提交
702 703 704
    return false unless locked?

    locked_at.nil? || locked_at < (Time.now - 1.day)
705
  end
706 707

  def has_ci?
708 709 710 711
    has_ci_integration = source_project.try(:ci_service)
    uses_gitlab_ci = all_pipelines.any?

    (has_ci_integration || uses_gitlab_ci) && commits.any?
712 713 714 715 716
  end

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

718
  def broken?
719
    has_no_commits? || branch_missing? || cannot_be_merged?
720 721
  end

722
  def can_be_merged_by?(user)
723 724 725 726 727 728 729
    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)
730 731
  end

732
  def mergeable_ci_state?
J
James Lopez 已提交
733
    return true unless project.only_allow_merge_if_pipeline_succeeds?
734

735
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
736 737
  end

D
Douwe Maan 已提交
738
  def environments_for(current_user)
739
    return [] unless diff_head_commit
740

D
Douwe Maan 已提交
741 742 743
    @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
744

D
Douwe Maan 已提交
745 746 747 748
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
749

D
Douwe Maan 已提交
750
      h[current_user] = envs.uniq
751
    end
D
Douwe Maan 已提交
752 753

    @environments[current_user]
754 755
  end

756 757 758 759 760 761 762 763 764
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
765

766 767 768 769 770 771 772 773 774 775
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

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

785 786 787 788
  def ref_path
    "refs/merge-requests/#{iid}/head"
  end

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

        computed_value
      end
797 798 799
  end

  def ensure_ref_fetched
800
    fetch_ref unless ref_fetched?
801 802
  end

803 804 805 806 807 808 809 810
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
      unlock_mr if locked?
    end
  end
811

812 813 814
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

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

830
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
831
  end
832
  private :compute_diverged_commits_count
833 834 835 836 837

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

838
  def all_pipelines
839
    return Ci::Pipeline.none unless source_project
840

841
    @all_pipelines ||= source_project.pipelines
842 843
      .where(sha: all_commits_sha, ref: source_branch)
      .order(id: :desc)
844
  end
845

846
  # Note that this could also return SHA from now dangling commits
847
  #
848
  def all_commits_sha
849 850
    if persisted?
      merge_request_diffs.flat_map(&:commits_sha).uniq
851 852
    elsif compare_commits
      compare_commits.to_a.reverse.map(&:id)
853
    else
854
      [diff_head_sha]
855
    end
856 857
  end

858 859 860 861
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

862
  def can_be_reverted?(current_user)
863
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
864
  end
865 866

  def can_be_cherry_picked?
F
Fatih Acet 已提交
867
    merge_commit.present?
868
  end
869

870
  def has_complete_diff_refs?
871
    diff_refs && diff_refs.complete?
872 873
  end

874
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
875
    return unless has_complete_diff_refs?
876 877
    return if new_diff_refs == old_diff_refs

878 879
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
880
    end
881
    return if active_diff_discussions.empty?
882

883
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
884

885
    service = Discussions::UpdateDiffPositionService.new(
886
      self.project,
887
      current_user,
888 889 890 891 892
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

893 894
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
895 896 897
    end
  end

898 899 900
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
901

902
  def has_commits?
903
    merge_request_diff && commits_count > 0
904 905 906 907 908
  end

  def has_no_commits?
    !has_commits?
  end
909

910
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
911 912 913 914 915 916 917 918 919 920
    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 已提交
921
end