Read about our upcoming Code of Conduct on this issue

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

8
9
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
10
  belongs_to :merge_user, class_name: "User"
11

12
  has_many :merge_request_diffs, dependent: :destroy
13
14
15
  has_one :merge_request_diff,
    -> { order('merge_request_diffs.id DESC') }

16
17
  has_many :events, as: :target, dependent: :destroy

18
  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
19

20
21
  serialize :merge_params, Hash

22
23
  after_create :ensure_merge_request_diff, unless: :importing?
  after_update :reload_diff_if_branch_changed
24

25
26
  delegate :commits, :real_size, :commits_sha, :commits_count,
    to: :merge_request_diff, prefix: nil
27

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
28
29
30
31
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

32
33
  # Temporary fields to store compare vars
  # when creating new merge request
34
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
35

Andrew8xx8's avatar
Andrew8xx8 committed
36
  state_machine :state, initial: :opened do
37
38
39
40
    event :close do
      transition [:reopened, :opened] => :closed
    end

41
    event :mark_as_merged do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
42
      transition [:reopened, :opened, :locked] => :merged
43
44
45
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
46
      transition closed: :reopened
47
48
    end

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

53
    event :unlock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
54
55
56
      transition locked: :reopened
    end

57
58
59
60
61
    after_transition any => :locked do |merge_request, transition|
      merge_request.locked_at = Time.now
      merge_request.save
    end

62
    after_transition locked: (any - :locked) do |merge_request, transition|
63
64
65
66
      merge_request.locked_at = nil
      merge_request.save
    end

67
68
69
70
    state :opened
    state :reopened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
71
    state :locked
72
73
  end

74
75
76
77
78
79
  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
80
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
81
82
83
    end

    event :mark_as_unmergeable do
84
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
85
86
    end

87
    state :unchecked
88
89
    state :can_be_merged
    state :cannot_be_merged
90
91

    around_transition do |merge_request, transition, block|
92
      Gitlab::Timeless.timeless(merge_request, &block)
93
    end
94
  end
95

96
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
97
  validates :source_branch, presence: true
98
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
99
  validates :target_branch, presence: true
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
100
  validates :merge_user, presence: true, if: :merge_when_build_succeeds?
101
102
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
103

104
  scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
105
  scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
106
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
107
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
108
  scope :from_project, ->(project) { where(source_project_id: project.id) }
109
110
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
111
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
112

113
114
115
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }

116
117
  after_save :keep_around_commit

118
119
120
121
  def self.reference_prefix
    '!'
  end

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

132
  def self.link_reference_pattern
133
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
134
135
  end

136
137
138
139
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

140
141
142
143
  def self.project_foreign_key
    'target_project_id'
  end

144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
  # 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)
    source = where(source_project_id: relation).select(:id)
    target = where(target_project_id: relation).select(:id)
    union  = Gitlab::SQL::Union.new([source, target])

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

Thomas Balthazar's avatar
Thomas Balthazar committed
162
163
164
165
166
167
168
169
170
171
172
173
174
175
  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

176
177
178
  def to_reference(from_project = nil)
    reference = "#{self.class.reference_prefix}#{iid}"

179
    "#{project.to_reference(from_project)}#{reference}"
180
181
  end

182
183
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
184
  end
185

186
  def raw_diffs(*args)
187
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
188
189
  end

190
  def diffs(diff_options = nil)
191
192
    if compare
      compare.diffs(diff_options)
193
    else
194
      merge_request_diff.diffs(diff_options)
195
    end
196
197
  end

198
  def diff_size
199
    diffs(diff_options).size
200
201
  end

202
  def diff_base_commit
203
    if persisted?
204
      merge_request_diff.base_commit
205
206
    else
      branch_merge_base_commit
207
208
209
210
211
212
213
214
215
    end
  end

  # MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
  # but we need to get a commit for the "View file @ ..." link by deleted files,
  # so we find the likely one if we can't get the actual one.
  # This will not be the actual base commit if the target branch was merged into
  # the source branch after the merge request was created, but it is good enough
  # for the specific purpose of linking to a commit.
Douwe Maan's avatar
Douwe Maan committed
216
217
218
  # It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
  # true base commit, so we can't simply have `#diff_base_commit` fall back on
  # this method.
219
220
221
222
223
224
225
226
227
  def likely_diff_base_commit
    first_commit.parent || first_commit
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
228
229
230
    end
  end

231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
  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
    source_branch_ref = @source_branch_sha || source_branch
259
    source_project.repository.commit(source_branch_ref) if source_branch_ref
260
261
262
263
  end

  def target_branch_head
    target_branch_ref = @target_branch_sha || target_branch
264
    target_project.repository.commit(target_branch_ref) if target_branch_ref
265
266
  end

267
268
269
270
271
272
273
274
275
  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

276
  def target_branch_sha
277
    @target_branch_sha || target_branch_head.try(:sha)
278
279
280
  end

  def source_branch_sha
281
    @source_branch_sha || source_branch_head.try(:sha)
282
283
  end

284
285
286
287
288
289
290
291
  def diff_refs
    return unless diff_start_commit || diff_base_commit

    Gitlab::Diff::DiffRefs.new(
      base_sha:  diff_base_sha,
      start_sha: diff_start_sha,
      head_sha:  diff_head_sha
    )
292
293
  end

294
295
  # Return diff_refs instance trying to not touch the git repository
  def diff_sha_refs
296
    if merge_request_diff && merge_request_diff.diff_refs_by_sha?
297
      merge_request_diff.diff_refs
298
    else
299
      diff_refs
300
    end
301
302
  end

303
304
305
306
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

307
  def validate_branches
308
    if target_project == source_project && target_branch == source_branch
309
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
310
    end
311

312
    if opened? || reopened?
313
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
314
315
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
316
        errors.add :validate_branches,
Gabriel Mazetto's avatar
Gabriel Mazetto committed
317
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
318
      end
319
    end
320
321
  end

322
  def validate_fork
323
    return true unless target_project && source_project
324
    return true if target_project == source_project
325
    return true unless source_project_missing?
326

327
    errors.add :validate_fork,
328
               'Source project is not a fork of the target project'
329
330
331
  end

  def closed_without_fork?
332
    closed? && source_project_missing?
333
334
  end

335
  def source_project_missing?
336
337
338
339
    return false unless for_fork?
    return true unless source_project

    !source_project.forked_from?(target_project)
340
341
  end

342
  def reopenable?
343
    closed? && !source_project_missing? && source_branch_exists?
Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
344
345
  end

346
347
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
348
349
  end

350
351
352
353
354
355
356
357
358
  def create_merge_request_diff
    merge_request_diffs.create
    reload_merge_request_diff
  end

  def reload_merge_request_diff
    merge_request_diff(true)
  end

359
  def reload_diff_if_branch_changed
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
360
    if source_branch_changed? || target_branch_changed?
361
      reload_diff
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
362
363
364
    end
  end

365
  def reload_diff
366
367
    return unless open?

368
    old_diff_refs = self.diff_refs
369
    create_merge_request_diff
370
    MergeRequests::MergeRequestDiffCacheService.new.execute(self)
371
372
373
374
375
376
    new_diff_refs = self.diff_refs

    update_diff_notes_positions(
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs
    )
377
378
  end

379
  def check_if_can_be_merged
380
381
    return unless unchecked?

382
    can_be_merged =
383
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
384
385

    if can_be_merged
386
387
388
389
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
390
391
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
392
  def merge_event
393
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
394
395
  end

396
  def closed_event
397
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
398
399
  end

400
  def work_in_progress?
Thomas Balthazar's avatar
Thomas Balthazar committed
401
    self.class.work_in_progress?(title)
402
403
404
  end

  def wipless_title
Thomas Balthazar's avatar
Thomas Balthazar committed
405
406
407
408
409
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
410
411
  end

412
413
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
414
415
416
417

    check_if_can_be_merged

    can_be_merged?
418
419
  end

420
  def mergeable_state?(skip_ci_check: false)
421
422
423
    return false unless open?
    return false if work_in_progress?
    return false if broken?
424
    return false unless skip_ci_check || mergeable_ci_state?
425
    return false unless mergeable_discussions_state?
426
427

    true
428
429
  end

430
431
  def can_cancel_merge_when_build_succeeds?(current_user)
    can_be_merged_by?(current_user) || self.author == current_user
432
433
  end

434
435
436
  def can_remove_source_branch?(current_user)
    !source_project.protected_branch?(source_branch) &&
      !source_project.root_ref?(source_branch) &&
http://jneen.net/'s avatar
http://jneen.net/ committed
437
      Ability.allowed?(current_user, :push_code, source_project) &&
438
      diff_head_commit == source_branch_head
439
440
  end

441
  def should_remove_source_branch?
442
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
443
444
445
  end

  def force_remove_source_branch?
446
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
447
448
449
450
451
452
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

453
  def mr_and_commit_notes
454
455
456
457
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
    commit_ids = commits.last(commits_for_notes_limit).map(&:id)

458
459
    Note.where(
      "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
460
      "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
461
      mr_id: id,
462
463
464
      commit_ids: commit_ids,
      target_project_id: target_project_id,
      source_project_id: source_project_id
465
    )
466
  end
467

468
  def discussions
469
    @discussions ||= self.mr_and_commit_notes.
470
      inc_relations_for_view.
471
472
473
474
      fresh.
      discussions
  end

475
476
477
478
479
480
481
482
483
  def diff_discussions
    @diff_discussions ||= self.notes.diff_notes.discussions
  end

  def find_diff_discussion(discussion_id)
    notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
    return if notes.empty?

    Discussion.new(notes)
484
485
  end

486
  def discussions_resolvable?
487
    diff_discussions.any?(&:resolvable?)
488
489
  end

490
  def discussions_resolved?
491
    discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
492
493
  end

494
495
496
497
  def discussions_to_be_resolved?
    discussions_resolvable? && !discussions_resolved?
  end

498
499
500
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

501
    !discussions_to_be_resolved?
502
503
  end

Kirill Zaitsev's avatar
Kirill Zaitsev committed
504
505
  def hook_attrs
    attrs = {
506
      source: source_project.try(:hook_attrs),
Kirill Zaitsev's avatar
Kirill Zaitsev committed
507
      target: target_project.hook_attrs,
508
509
      last_commit: nil,
      work_in_progress: work_in_progress?
Kirill Zaitsev's avatar
Kirill Zaitsev committed
510
511
    }

512
513
    if diff_head_commit
      attrs.merge!(last_commit: diff_head_commit.hook_attrs)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
514
515
516
517
518
    end

    attributes.merge!(attrs)
  end

519
520
521
522
  def for_fork?
    target_project != source_project
  end

523
524
525
526
  def project
    target_project
  end

527
528
529
530
  # 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.
531
  # This optimization does not apply to issues from external sources.
532
  def cache_merge_request_closes_issues!(current_user = self.author)
533
534
    return if project.has_external_issue_tracker?

535
    transaction do
536
      self.merge_requests_closing_issues.delete_all
537

538
      closes_issues(current_user).each do |issue|
539
        self.merge_requests_closing_issues.create!(issue: issue)
540
541
542
543
      end
    end
  end

544
545
546
547
  def closes_issue?(issue)
    closes_issues.include?(issue)
  end

548
  # Return the set of issues that will be closed if this merge request is accepted.
549
  def closes_issues(current_user = self.author)
550
    if target_branch == project.default_branch
551
552
      messages = [description]
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
553
554
555

      Gitlab::ClosingIssueExtractor.new(project, current_user).
        closed_by_message(messages.join("\n"))
556
557
558
559
560
    else
      []
    end
  end

561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
  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

577
578
579
580
581
582
583
584
  def source_project_namespace
    if source_project && source_project.namespace
      source_project.namespace.path
    else
      "(removed)"
    end
  end

585
586
587
588
589
590
591
592
  def target_project_namespace
    if target_project && target_project.namespace
      target_project.namespace.path
    else
      "(removed)"
    end
  end

593
594
595
596
597
598
599
600
601
602
603
604
  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

605
  def merge_commit_message
606
607
608
609
610
    message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
    message << "#{title}\n\n"
    message << "#{description}\n\n" if description.present?
    message << "See merge request #{to_reference}"

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
611
    message
612
  end
613

614
615
  def reset_merge_when_build_succeeds
    return unless merge_when_build_succeeds?
616

617
618
    self.merge_when_build_succeeds = false
    self.merge_user = nil
619
620
621
622
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
    end
623
624
625
626

    self.save
  end

627
  # Return array of possible target branches
Steven Burgart's avatar
Steven Burgart committed
628
  # depends on target project of MR
629
630
631
632
633
634
635
636
637
  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
638
  # depends on source project of MR
639
640
641
642
643
644
645
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
646
647

  def locked_long_ago?
Ben Bodenmiller's avatar
Ben Bodenmiller committed
648
649
650
    return false unless locked?

    locked_at.nil? || locked_at < (Time.now - 1.day)
651
  end
652
653

  def has_ci?
654
    source_project.try(:ci_service) && commits.any?
655
656
657
658
659
  end

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

661
  def broken?
662
    has_no_commits? || branch_missing? || cannot_be_merged?
663
664
  end

665
  def can_be_merged_by?(user)
666
667
668
669
670
671
672
    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)
673
674
  end

675
676
  def mergeable_ci_state?
    return true unless project.only_allow_merge_if_build_succeeds?
677

678
    !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
679
680
  end

681
  def environments
682
    return [] unless diff_head_commit
683

684
685
686
687
688
689
690
691
692
    @environments ||= begin
      target_envs = target_project.environments_for(
        target_branch, commit: diff_head_commit, with_tags: true)

      source_envs = source_project.environments_for(
        source_branch, commit: diff_head_commit) if source_project

      (target_envs.to_a + source_envs.to_a).uniq
    end
693
694
  end

695
696
697
698
699
700
701
702
703
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
704

705
706
707
708
709
710
711
712
713
714
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

715
716
717
718
  def fetch_ref
    target_project.repository.fetch_ref(
      source_project.repository.path_to_repo,
      "refs/heads/#{source_branch}",
719
      ref_path
720
721
722
    )
  end

723
724
725
726
  def ref_path
    "refs/merge-requests/#{iid}/head"
  end

727
728
  def ref_fetched?
    project.repository.ref_exists?(ref_path)
729
730
731
  end

  def ensure_ref_fetched
732
    fetch_ref unless ref_fetched?
733
734
  end

735
736
737
738
739
740
741
742
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
      unlock_mr if locked?
    end
  end
743

744
745
746
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

747
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
748
      cache = {
749
750
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
751
752
753
754
755
756
757
758
759
        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
760
    return 0 unless source_branch_sha && target_branch_sha
761

762
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
763
  end
764
  private :compute_diverged_commits_count
765
766
767
768
769

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

770
  def head_pipeline
771
    return unless diff_head_sha && source_project
772

773
    @head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
774
  end
775

776
  def all_pipelines
777
    return Ci::Pipeline.none unless source_project
778

779
    @all_pipelines ||= source_project.pipelines
780
781
      .where(sha: all_commits_sha, ref: source_branch)
      .order(id: :desc)
782
  end
783

784
  # Note that this could also return SHA from now dangling commits
785
  #
786
  def all_commits_sha
787
788
    if persisted?
      merge_request_diffs.flat_map(&:commits_sha).uniq
789
790
    elsif compare_commits
      compare_commits.to_a.reverse.map(&:id)
791
    else
792
      [diff_head_sha]
793
    end
794
795
  end

796
797
798
799
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

800
801
  def can_be_reverted?(current_user = nil)
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
802
  end
803
804
805
806

  def can_be_cherry_picked?
    merge_commit
  end
807

808
  def has_complete_diff_refs?
809
    diff_sha_refs && diff_sha_refs.complete?
810
811
  end

812
  def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
813
    return unless has_complete_diff_refs?
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
    return if new_diff_refs == old_diff_refs

    active_diff_notes = self.notes.diff_notes.select do |note|
      note.new_diff_note? && note.active?(old_diff_refs)
    end

    return if active_diff_notes.empty?

    paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq

    service = Notes::DiffPositionUpdateService.new(
      self.project,
      nil,
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

    active_diff_notes.each do |note|
      service.execute(note)
      Gitlab::Timeless.timeless(note, &:save)
    end
  end

838
839
840
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
841
842
843
844
845

  def conflicts
    @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
  end

846
847
848
849
850
  def conflicts_can_be_resolved_by?(user)
    access = ::Gitlab::UserAccess.new(user, project: source_project)
    access.can_push_to_branch?(source_branch)
  end

851
852
853
854
855
  def conflicts_can_be_resolved_in_ui?
    return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)

    return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
    return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
856
857

    begin
858
859
860
861
862
      # Try to parse each conflict. If the MR's mergeable status hasn't been updated,
      # ensure that we don't say there are conflicts to resolve when there are no conflict
      # files.
      conflicts.files.each(&:lines)
      @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
863
    rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
864
      @conflicts_can_be_resolved_in_ui = false
865
866
    end
  end
867
868
869
870
871
872
873
874

  def has_commits?
    commits_count > 0
  end

  def has_no_commits?
    !has_commits?
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
875
end