merge_request.rb 27.1 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1
class MergeRequest < ActiveRecord::Base
2
  include InternalId
3
  include Issuable
4
  include Noteable
5
  include Referable
6
  include IgnorableColumn
7
  include TimeTrackable
8 9
  include ManualInverseAssociation
  include EachBatch
10

11 12
  ignore_column :locked_at,
                :ref_fetched
13

14 15
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
Z
Zeger-Jan van de Weg 已提交
16
  belongs_to :merge_user, class_name: "User"
17

18
  has_many :merge_request_diffs
19

20
  has_one :merge_request_diff,
21
    -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
22

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
  belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
  manual_inverse_association :latest_merge_request_diff, :merge_request

  # This is the same as latest_merge_request_diff unless:
  # 1. There are arguments - in which case we might be trying to force-reload.
  # 2. This association is already loaded.
  # 3. The latest diff does not exist.
  #
  # The second one in particular is important - MergeRequestDiff#merge_request
  # is the inverse of MergeRequest#merge_request_diff, which means it may not be
  # the latest diff, because we could have loaded any diff from this particular
  # MR. If we haven't already loaded a diff, then it's fine to load the latest.
  def merge_request_diff(*args)
    fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?

    fallback || super
  end

41 42
  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"

43
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
44

45 46 47
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
48

49 50
  belongs_to :assignee, class_name: "User"

51
  serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
Z
Zeger-Jan van de Weg 已提交
52

53 54
  after_create :ensure_merge_request_diff, unless: :importing?
  after_update :reload_diff_if_branch_changed
55

D
Dmitriy Zaporozhets 已提交
56 57 58 59
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

D
Dmitriy Zaporozhets 已提交
60 61
  # Temporary fields to store compare vars
  # when creating new merge request
62
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
D
Dmitriy Zaporozhets 已提交
63

A
Andrew8xx8 已提交
64
  state_machine :state, initial: :opened do
A
Andrew8xx8 已提交
65
    event :close do
66
      transition [:opened] => :closed
A
Andrew8xx8 已提交
67 68
    end

69
    event :mark_as_merged do
70
      transition [:opened, :locked] => :merged
A
Andrew8xx8 已提交
71 72 73
    end

    event :reopen do
74
      transition closed: :opened
A
Andrew8xx8 已提交
75 76
    end

77
    event :lock_mr do
78
      transition [:opened] => :locked
D
Dmitriy Zaporozhets 已提交
79 80
    end

81
    event :unlock_mr do
82
      transition locked: :opened
D
Dmitriy Zaporozhets 已提交
83 84
    end

A
Andrew8xx8 已提交
85 86 87
    state :opened
    state :closed
    state :merged
D
Dmitriy Zaporozhets 已提交
88
    state :locked
A
Andrew8xx8 已提交
89 90
  end

91 92 93 94 95 96
  state_machine :merge_status, initial: :unchecked do
    event :mark_as_unchecked do
      transition [:can_be_merged, :cannot_be_merged] => :unchecked
    end

    event :mark_as_mergeable do
97
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
98 99 100
    end

    event :mark_as_unmergeable do
101
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
102 103
    end

104
    state :unchecked
105 106
    state :can_be_merged
    state :cannot_be_merged
107 108

    around_transition do |merge_request, transition, block|
109
      Gitlab::Timeless.timeless(merge_request, &block)
110
    end
111
  end
112

113
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
A
Andrey Kumanyaev 已提交
114
  validates :source_branch, presence: true
I
Izaak Alpert 已提交
115
  validates :target_project, presence: true
A
Andrey Kumanyaev 已提交
116
  validates :target_branch, presence: true
J
James Lopez 已提交
117
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
118 119
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
120
  validate :validate_target_project, on: :create
D
Dmitriy Zaporozhets 已提交
121

122 123 124
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
125
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
126
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
127
  scope :from_project, ->(project) { where(source_project_id: project.id) }
128 129
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
S
Scott Le 已提交
130
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
131

132 133
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
134 135 136 137 138
  scope :assigned, -> { where("assignee_id IS NOT NULL") }
  scope :unassigned, -> { where("assignee_id IS NULL") }
  scope :assigned_to, ->(u) { where(assignee_id: u.id)}

  participant :assignee
139

140 141
  after_save :keep_around_commit

142 143
  acts_as_paranoid

144 145 146 147
  def self.reference_prefix
    '!'
  end

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

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

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

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

170 171 172 173 174 175 176 177 178 179 180
  # Returns all the merge requests from an ActiveRecord:Relation.
  #
  # This method uses a UNION as it usually operates on the result of
  # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
  # using multiple sub-queries especially when combined with an OR statement.
  # UNIONs on the other hand perform much better in these cases.
  #
  # relation - An ActiveRecord::Relation that returns a list of Projects.
  #
  # Returns an ActiveRecord::Relation.
  def self.in_projects(relation)
M
mhasbini 已提交
181 182 183 184
    # unscoping unnecessary conditions that'll be applied
    # when executing `where("merge_requests.id IN (#{union.to_sql})")`
    source = unscoped.where(source_project_id: relation).select(:id)
    target = unscoped.where(target_project_id: relation).select(:id)
185 186
    union  = Gitlab::SQL::Union.new([source, target])

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

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
  # This is used after project import, to reset the IDs to the correct
  # values. It is not intended to be called without having already scoped the
  # relation.
  def self.set_latest_merge_request_diff_ids!
    update = '
      latest_merge_request_diff_id = (
        SELECT MAX(id)
        FROM merge_request_diffs
        WHERE merge_requests.id = merge_request_diffs.merge_request_id
      )'.squish

    self.each_batch do |batch|
      batch.update_all(update)
    end
  end

T
Thomas Balthazar 已提交
206 207 208 209 210 211 212 213 214 215 216 217 218 219
  WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze

  def self.work_in_progress?(title)
    !!(title =~ WIP_REGEX)
  end

  def self.wipless_title(title)
    title.sub(WIP_REGEX, "")
  end

  def self.wip_title(title)
    work_in_progress?(title) ? title : "WIP: #{title}"
  end

220 221 222 223 224 225
  # Verifies if title has changed not taking into account WIP prefix
  # for merge requests.
  def wipless_title_changed(old_title)
    self.class.wipless_title(old_title) != self.wipless_title
  end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

367 368 369 370 371 372 373 374 375 376 377 378 379 380
  def source_branch_ref
    return @source_branch_sha if @source_branch_sha
    return unless source_branch

    Gitlab::Git::BRANCH_REF_PREFIX + source_branch
  end

  def target_branch_ref
    return @target_branch_sha if @target_branch_sha
    return unless target_branch

    Gitlab::Git::BRANCH_REF_PREFIX + target_branch
  end

381
  def source_branch_head
382 383
    return unless source_project

S
Sean McGivern 已提交
384
    source_project.repository.commit(source_branch_ref) if source_branch_ref
385 386 387
  end

  def target_branch_head
388
    target_project.repository.commit(target_branch_ref)
389 390
  end

391 392 393 394 395 396 397 398 399
  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

400
  def target_branch_sha
401
    @target_branch_sha || target_branch_head.try(:sha)
402 403 404
  end

  def source_branch_sha
405
    @source_branch_sha || source_branch_head.try(:sha)
406 407
  end

408
  def diff_refs
409
    if persisted?
410
      merge_request_diff.diff_refs
411
    else
412 413 414 415 416
      Gitlab::Diff::DiffRefs.new(
        base_sha:  diff_base_sha,
        start_sha: diff_start_sha,
        head_sha:  diff_head_sha
      )
417
    end
418 419
  end

420 421 422 423
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

424
  def validate_branches
425
    if target_project == source_project && target_branch == source_branch
I
Izaak Alpert 已提交
426
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
427
    end
428

429
    if opened?
430
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
431 432
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
J
jubianchi 已提交
433
        errors.add :validate_branches,
G
Gabriel Mazetto 已提交
434
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
435
      end
436
    end
437 438
  end

439 440 441 442 443 444
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

445
  def validate_fork
446
    return true unless target_project && source_project
447
    return true if target_project == source_project
448
    return true unless source_project_missing?
449

450
    errors.add :validate_fork,
451
               'Source project is not a fork of the target project'
452 453
  end

454
  def merge_ongoing?
455 456
    # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
    # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
457 458 459
    return true if locked?

    !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
460 461
  end

462
  def closed_without_fork?
463
    closed? && source_project_missing?
464 465
  end

466
  def source_project_missing?
467 468 469
    return false unless for_fork?
    return true unless source_project

470
    !source_project.in_fork_network_of?(target_project)
471 472
  end

473
  def reopenable?
474
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
475 476
  end

477 478
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
479 480
  end

481
  def create_merge_request_diff
482
    fetch_ref!
483

484 485 486 487 488
    # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
    Gitlab::GitalyClient.allow_n_plus_1_calls do
      merge_request_diffs.create
      reload_merge_request_diff
    end
489 490 491 492 493 494
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

495 496
  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|
497
      diffs = merge_request_diffs.viewable
498 499 500 501 502 503
      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 已提交
504 505
    end

506
    @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
507 508
  end

509 510 511 512 513 514 515 516 517 518 519
  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

520
  def reload_diff_if_branch_changed
521 522
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
523
      reload_diff
D
Dmitriy Zaporozhets 已提交
524 525 526
    end
  end

527
  def reload_diff(current_user = nil)
528 529
    return unless open?

530
    old_diff_refs = self.diff_refs
531

532
    create_merge_request_diff
533
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
534 535
    new_diff_refs = self.diff_refs

536
    update_diff_discussion_positions(
537
      old_diff_refs: old_diff_refs,
538 539
      new_diff_refs: new_diff_refs,
      current_user: current_user
540
    )
541 542
  end

543
  def check_if_can_be_merged
T
Toon Claes 已提交
544
    return unless unchecked? && Gitlab::Database.read_write?
545

546
    can_be_merged =
547
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
548 549

    if can_be_merged
550 551 552 553
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
554 555
  end

D
Dmitriy Zaporozhets 已提交
556
  def merge_event
557
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
558 559
  end

560
  def closed_event
561
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
562 563
  end

564
  def work_in_progress?
T
Thomas Balthazar 已提交
565
    self.class.work_in_progress?(title)
566 567 568
  end

  def wipless_title
T
Thomas Balthazar 已提交
569 570 571 572 573
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
574 575
  end

576 577
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
578 579 580 581

    check_if_can_be_merged

    can_be_merged?
582 583
  end

584
  def mergeable_state?(skip_ci_check: false)
585 586 587
    return false unless open?
    return false if work_in_progress?
    return false if broken?
588
    return false unless skip_ci_check || mergeable_ci_state?
589
    return false unless mergeable_discussions_state?
590 591

    true
592 593
  end

594 595 596 597 598 599 600 601
  def ff_merge_possible?
    project.repository.ancestor?(target_branch_sha, diff_head_sha)
  end

  def should_be_rebased?
    project.ff_merge_must_be_possible? && !ff_merge_possible?
  end

J
James Lopez 已提交
602
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
603
    can_be_merged_by?(current_user) || self.author == current_user
604 605
  end

606
  def can_remove_source_branch?(current_user)
607
    !ProtectedBranch.protected?(source_project, source_branch) &&
608
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
609
      Ability.allowed?(current_user, :push_code, source_project) &&
610
      diff_head_commit == source_branch_head
611 612
  end

613
  def should_remove_source_branch?
614
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
615 616 617
  end

  def force_remove_source_branch?
618
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
619 620 621 622 623 624
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

625
  def related_notes
626 627
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
628
    commit_ids = commit_shas.take(commits_for_notes_limit)
629

630 631 632
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
633
      .for_commit_id(commit_ids)
634 635 636 637 638 639 640 641 642 643

    # We're using a UNION ALL here since this results in better performance
    # compared to using OR statements. We're using UNION ALL since the queries
    # used won't produce any duplicates (e.g. a note for a commit can't also be
    # a note for an MR).
    union = Gitlab::SQL::Union
      .new([notes, commit_notes], remove_duplicates: false)
      .to_sql

    Note.from("(#{union}) #{Note.table_name}")
644
  end
645

646
  alias_method :discussion_notes, :related_notes
647

648 649 650
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

651
    !discussions_to_be_resolved?
652 653
  end

I
Izaak Alpert 已提交
654 655 656 657
  def for_fork?
    target_project != source_project
  end

658 659 660 661
  def project
    target_project
  end

662 663 664 665
  # 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.
666
  # This optimization does not apply to issues from external sources.
667
  def cache_merge_request_closes_issues!(current_user)
668
    return unless project.issues_enabled?
669

670
    transaction do
671
      self.merge_requests_closing_issues.delete_all
672

673
      closes_issues(current_user).each do |issue|
674 675
        next if issue.is_a?(ExternalIssue)

676
        self.merge_requests_closing_issues.create!(issue: issue)
677 678 679 680
      end
    end
  end

681
  # Return the set of issues that will be closed if this merge request is accepted.
682
  def closes_issues(current_user = self.author)
683
    if target_branch == project.default_branch
684
      messages = [title, description]
685
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
686

687 688
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
689 690 691 692 693
    else
      []
    end
  end

694
  def issues_mentioned_but_not_closing(current_user)
695
    return [] unless target_branch == project.default_branch
696

697
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
698
    ext.analyze("#{title}\n#{description}")
699

700
    ext.issues - closes_issues(current_user)
701 702
  end

703 704
  def target_project_path
    if target_project
705
      target_project.full_path
706 707 708 709 710 711 712
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
713
      source_project.full_path
714 715 716 717 718
    else
      "(removed)"
    end
  end

719 720
  def source_project_namespace
    if source_project && source_project.namespace
721
      source_project.namespace.full_path
722 723 724 725 726
    else
      "(removed)"
    end
  end

727 728
  def target_project_namespace
    if target_project && target_project.namespace
729
      target_project.namespace.full_path
730 731 732 733 734
    else
      "(removed)"
    end
  end

735 736 737
  def source_branch_exists?
    return false unless self.source_project

738
    self.source_project.repository.branch_exists?(self.source_branch)
739 740 741 742 743
  end

  def target_branch_exists?
    return false unless self.target_project

744
    self.target_project.repository.branch_exists?(self.target_branch)
745 746
  end

747
  def merge_commit_message(include_description: false)
748 749 750 751
    closes_issues_references = closes_issues.map do |issue|
      issue.to_reference(target_project)
    end

752 753 754 755
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
756

757
    if !include_description && closes_issues_references.present?
758
      message << "Closes #{closes_issues_references.to_sentence}"
759
    end
760
    message << "#{description}" if include_description && description.present?
761
    message << "See merge request #{to_reference(full: true)}"
762

763
    message.join("\n\n")
764
  end
765

J
James Lopez 已提交
766 767
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
768

J
James Lopez 已提交
769
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
770
    self.merge_user = nil
771 772 773 774
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
Z
Zeger-Jan van de Weg 已提交
775 776 777 778

    self.save
  end

779
  # Return array of possible target branches
S
Steven Burgart 已提交
780
  # depends on target project of MR
781 782 783 784 785 786 787 788 789
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
790
  # depends on source project of MR
791 792 793 794 795 796 797
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
798

799
  def has_ci?
800
    return false if has_no_commits?
801

802
    !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
803 804 805 806 807
  end

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

809
  def broken?
810
    has_no_commits? || branch_missing? || cannot_be_merged?
811 812
  end

813
  def can_be_merged_by?(user)
814 815 816 817 818 819 820
    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)
821 822
  end

823
  def mergeable_ci_state?
J
James Lopez 已提交
824
    return true unless project.only_allow_merge_if_pipeline_succeeds?
825

826
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
827 828
  end

D
Douwe Maan 已提交
829
  def environments_for(current_user)
830
    return [] unless diff_head_commit
831

D
Douwe Maan 已提交
832 833 834
    @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
835

D
Douwe Maan 已提交
836 837 838 839
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
840

D
Douwe Maan 已提交
841
      h[current_user] = envs.uniq
842
    end
D
Douwe Maan 已提交
843 844

    @environments[current_user]
845 846
  end

847 848 849 850 851 852 853 854 855
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
856

857 858 859 860 861 862 863 864 865 866
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

867 868
  def fetch_ref!
    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
869 870
  end

871
  def ref_path
872
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
873 874
  end

875 876 877 878 879
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
880
      unlock_mr
881 882
    end
  end
883

884 885 886
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

887
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
888
      cache = {
889 890
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
891 892 893 894 895 896 897 898 899
        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
900
    return 0 unless source_branch_sha && target_branch_sha
901

L
Lin Jen-Shin 已提交
902 903
    target_project.repository
      .count_commits_between(source_branch_sha, target_branch_sha)
904
  end
905
  private :compute_diverged_commits_count
906 907 908 909 910

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

911
  def all_pipelines
912
    return Ci::Pipeline.none unless source_project
913

914
    @all_pipelines ||= source_project.pipelines
915
      .where(sha: all_commit_shas, ref: source_branch)
916
      .order(id: :desc)
917
  end
918

919
  # Note that this could also return SHA from now dangling commits
920
  #
921
  def all_commit_shas
922
    return commit_shas unless persisted?
923

924
    diffs_relation = merge_request_diffs
925

926 927
    # MySQL doesn't support LIMIT in a subquery.
    diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
928

929 930 931 932 933
    MergeRequestDiffCommit
      .where(merge_request_diff: diffs_relation)
      .limit(10_000)
      .pluck('sha')
      .uniq
934 935
  end

936 937 938 939
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

940
  def can_be_reverted?(current_user)
941
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
942
  end
943 944

  def can_be_cherry_picked?
F
Fatih Acet 已提交
945
    merge_commit.present?
946
  end
947

948
  def has_complete_diff_refs?
949
    diff_refs && diff_refs.complete?
950 951
  end

952
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
953
    return unless has_complete_diff_refs?
954 955
    return if new_diff_refs == old_diff_refs

956 957
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
958
    end
959
    return if active_diff_discussions.empty?
960

961
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
962

963
    service = Discussions::UpdateDiffPositionService.new(
964
      self.project,
965
      current_user,
966 967 968 969 970
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

971 972
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
973
    end
974 975 976 977 978 979

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

982 983 984
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
985

986
  def has_commits?
987
    merge_request_diff && commits_count > 0
988 989 990 991 992
  end

  def has_no_commits?
    !has_commits?
  end
993

994
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
995 996 997 998 999 1000 1001 1002 1003 1004
    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
1005

1006 1007 1008 1009
  def update_project_counter_caches
    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
  end

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

M
micael.bergeron 已提交
1013 1014
    project.merge_requests.merged.where(author_id: author_id).empty?
  end
D
Dmitriy Zaporozhets 已提交
1015
end