Read about our upcoming Code of Conduct on this issue

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

  ignore_column :position
10

11
12
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
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
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
34

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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

39
40
  # Temporary fields to store compare vars
  # when creating new merge request
41
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
42

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

48
    event :mark_as_merged do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
49
      transition [:reopened, :opened, :locked] => :merged
50
51
52
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
53
      transition closed: :reopened
54
55
    end

56
    event :lock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
57
58
59
      transition [:reopened, :opened] => :locked
    end

60
    event :unlock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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

74
75
76
77
    state :opened
    state :reopened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
78
    state :locked
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?]
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
104
  validates :source_branch, presence: true
105
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
106
  validates :target_branch, presence: true
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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) }
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)
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

Thomas Balthazar's avatar
Thomas Balthazar committed
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)
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
241
242
  end

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
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
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
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
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?
347
        errors.add :validate_branches,
Gabriel Mazetto's avatar
Gabriel Mazetto committed
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?
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
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
Douwe Maan's avatar
Douwe Maan committed
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
422
    if source_branch_changed? || target_branch_changed?
423
      reload_diff
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
455
  def merge_event
456
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
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?
Thomas Balthazar's avatar
Thomas Balthazar committed
464
    self.class.work_in_progress?(title)
465
466
467
  end

  def wipless_title
Thomas Balthazar's avatar
Thomas Balthazar committed
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

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) &&
http://jneen.net/'s avatar
http://jneen.net/ committed
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

Kirill Zaitsev's avatar
Kirill Zaitsev committed
539
540
  def hook_attrs
    attrs = {
541
      source: source_project.try(:hook_attrs),
Kirill Zaitsev's avatar
Kirill Zaitsev committed
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
Kirill Zaitsev's avatar
Kirill Zaitsev committed
548
549
    }

550
    if diff_head_commit
Douwe Maan's avatar
Douwe Maan committed
551
      attrs[:last_commit] = diff_head_commit.hook_attrs
Kirill Zaitsev's avatar
Kirill Zaitsev committed
552
553
554
555
556
    end

    attributes.merge!(attrs)
  end

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

668
669
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
670

671
    self.merge_when_pipeline_succeeds = false
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
677
678
679
680

    self.save
  end

681
  # Return array of possible target branches
Steven Burgart's avatar
Steven Burgart committed
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
Steven Burgart's avatar
Steven Burgart committed
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?
Ben Bodenmiller's avatar
Ben Bodenmiller committed
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?
733
    return true unless project.only_allow_merge_if_pipeline_succeeds?
734

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

Douwe Maan's avatar
Douwe Maan committed
738
  def environments_for(current_user)
739
    return [] unless diff_head_commit
740

Douwe Maan's avatar
Douwe Maan committed
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

Douwe Maan's avatar
Douwe Maan committed
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

Douwe Maan's avatar
Douwe Maan committed
750
      h[current_user] = envs.uniq
751
    end
Douwe Maan's avatar
Douwe Maan committed
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?
Fatih Acet's avatar
Fatih Acet committed
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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
921
end