merge_request.rb 39.3 KB
Newer Older
1 2
# frozen_string_literal: true

D
Dmitriy Zaporozhets 已提交
3
class MergeRequest < ActiveRecord::Base
4
  include AtomicInternalId
S
Shinya Maeda 已提交
5
  include IidRoutes
6
  include Issuable
7
  include Noteable
8
  include Referable
9
  include Presentable
10
  include IgnorableColumn
11
  include TimeTrackable
12 13
  include ManualInverseAssociation
  include EachBatch
14
  include ThrottledTouch
15
  include Gitlab::Utils::StrongMemoize
J
Jan Provaznik 已提交
16
  include LabelEventable
S
Shinya Maeda 已提交
17
  include ReactiveCaching
18
  include FromUnion
S
Shinya Maeda 已提交
19 20

  self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
S
Shinya Maeda 已提交
21 22
  self.reactive_cache_refresh_interval = 10.minutes
  self.reactive_cache_lifetime = 10.minutes
23

24 25
  SORTING_PREFERENCE_FIELD = :merge_requests_sort

26
  ignore_column :locked_at,
27 28
                :ref_fetched,
                :deleted_at
29

30 31
  belongs_to :target_project, class_name: "Project"
  belongs_to :source_project, class_name: "Project"
Z
Zeger-Jan van de Weg 已提交
32
  belongs_to :merge_user, class_name: "User"
33

34 35
  has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }

36
  has_many :merge_request_diffs
37

38
  has_one :merge_request_diff,
39
    -> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
40

41 42 43 44 45 46 47 48 49 50 51 52
  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.
53 54
  def merge_request_diff
    fallback = latest_merge_request_diff unless association(:merge_request_diff).loaded?
55 56 57 58

    fallback || super
  end

59 60
  belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"

61
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
62

63 64 65
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
66

67
  has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
S
Shinya Maeda 已提交
68
  has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
69

70 71
  belongs_to :assignee, class_name: "User"

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

74
  after_create :ensure_merge_request_diff
75
  after_update :clear_memoized_shas
76
  after_update :reload_diff_if_branch_changed
77
  after_save :ensure_metrics
78

D
Dmitriy Zaporozhets 已提交
79 80 81 82
  # 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 已提交
83 84
  # Temporary fields to store compare vars
  # when creating new merge request
85
  attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
D
Dmitriy Zaporozhets 已提交
86

A
Andrew8xx8 已提交
87
  state_machine :state, initial: :opened do
A
Andrew8xx8 已提交
88
    event :close do
89
      transition [:opened] => :closed
A
Andrew8xx8 已提交
90 91
    end

92
    event :mark_as_merged do
93
      transition [:opened, :locked] => :merged
A
Andrew8xx8 已提交
94 95 96
    end

    event :reopen do
97
      transition closed: :opened
A
Andrew8xx8 已提交
98 99
    end

100
    event :lock_mr do
101
      transition [:opened] => :locked
D
Dmitriy Zaporozhets 已提交
102 103
    end

104
    event :unlock_mr do
105
      transition locked: :opened
D
Dmitriy Zaporozhets 已提交
106 107
    end

108 109
    before_transition any => :opened do |merge_request|
      merge_request.merge_jid = nil
110
    end
111

112
    after_transition any => :opened do |merge_request|
113 114 115 116 117
      merge_request.run_after_commit do
        UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
      end
    end

A
Andrew8xx8 已提交
118 119 120
    state :opened
    state :closed
    state :merged
D
Dmitriy Zaporozhets 已提交
121
    state :locked
A
Andrew8xx8 已提交
122 123
  end

124 125
  state_machine :merge_status, initial: :unchecked do
    event :mark_as_unchecked do
126 127
      transition [:can_be_merged, :unchecked] => :unchecked
      transition [:cannot_be_merged, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
128 129 130
    end

    event :mark_as_mergeable do
131
      transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged
132 133 134
    end

    event :mark_as_unmergeable do
135
      transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged
136 137
    end

138
    state :unchecked
139
    state :cannot_be_merged_recheck
140 141
    state :can_be_merged
    state :cannot_be_merged
142 143

    around_transition do |merge_request, transition, block|
144
      Gitlab::Timeless.timeless(merge_request, &block)
145
    end
146

147
    # rubocop: disable CodeReuse/ServiceClass
148
    after_transition unchecked: :cannot_be_merged do |merge_request, transition|
149 150 151
      if merge_request.notify_conflict?
        NotificationService.new.merge_request_unmergeable(merge_request)
        TodoService.new.merge_request_became_unmergeable(merge_request)
152
      end
153
    end
154
    # rubocop: enable CodeReuse/ServiceClass
155

156 157 158
    def check_state?(merge_status)
      [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
    end
159
  end
160

161
  validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
A
Andrey Kumanyaev 已提交
162
  validates :source_branch, presence: true
I
Izaak Alpert 已提交
163
  validates :target_project, presence: true
A
Andrey Kumanyaev 已提交
164
  validates :target_branch, presence: true
J
James Lopez 已提交
165
  validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
166 167
  validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
  validate :validate_fork, unless: :closed_without_fork?
168
  validate :validate_target_project, on: :create
D
Dmitriy Zaporozhets 已提交
169

170 171 172
  scope :by_source_or_target_branch, ->(branch_name) do
    where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
  end
173
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
174
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
175
  scope :from_project, ->(project) { where(source_project_id: project.id) }
176 177
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
S
Scott Le 已提交
178
  scope :from_source_branches, ->(branches) { where(source_branch: branches) }
179 180 181
  scope :by_commit_sha, ->(sha) do
    where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
  end
182 183
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }
184 185 186
  scope :assigned, -> { where("assignee_id IS NOT NULL") }
  scope :unassigned, -> { where("assignee_id IS NULL") }
  scope :assigned_to, ->(u) { where(assignee_id: u.id)}
187 188 189 190 191 192 193
  scope :with_api_entity_associations, -> {
    preload(:author, :assignee, :notes, :labels, :milestone, :timelogs,
            latest_merge_request_diff: [:merge_request_diff_commits],
            metrics: [:latest_closed_by, :merged_by],
            target_project: [:route, { namespace: :route }],
            source_project: [:route, { namespace: :route }])
  }
194 195

  participant :assignee
196

197 198
  after_save :keep_around_commit

199 200 201
  alias_attribute :project, :target_project
  alias_attribute :project_id, :target_project_id

202 203 204 205
  def self.reference_prefix
    '!'
  end

206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
  # Returns the top 100 target branches
  #
  # The returned value is a Array containing branch names
  # sort by updated_at of merge request:
  #
  #     ['master', 'develop', 'production']
  #
  # limit - The maximum number of target branch to return.
  def self.recent_target_branches(limit: 100)
    group(:target_branch)
      .select(:target_branch)
      .reorder('MAX(merge_requests.updated_at) DESC')
      .limit(limit)
      .pluck(:target_branch)
  end

222
  def rebase_in_progress?
223 224 225
    strong_memoize(:rebase_in_progress) do
      # The source project can be deleted
      next false unless source_project
226

227 228
      source_project.repository.rebase_in_progress?(id)
    end
229 230
  end

231 232 233
  # Use this method whenever you need to make sure the head_pipeline is synced with the
  # branch head commit, for example checking if a merge request can be merged.
  # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
234
  def actual_head_pipeline
235
    head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
236 237
  end

238 239 240 241 242 243
  def merge_pipeline
    return unless merged?

    target_project.pipeline_for(target_branch, merge_commit_sha)
  end

244 245 246 247
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
248
    @reference_pattern ||= %r{
249
      (#{Project.reference_pattern})?
250 251 252 253
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

254
  def self.link_reference_pattern
255
    @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
256 257
  end

258 259 260 261
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

262 263 264 265
  def self.project_foreign_key
    'target_project_id'
  end

266 267 268 269 270 271 272 273 274 275 276
  # 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 已提交
277 278
    # unscoping unnecessary conditions that'll be applied
    # when executing `where("merge_requests.id IN (#{union.to_sql})")`
279 280
    source = unscoped.where(source_project_id: relation)
    target = unscoped.where(target_project_id: relation)
281

282
    from_union([source, target])
283 284
  end

285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  # 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

301
  WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
T
Thomas Balthazar 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314

  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

S
Stan Hu 已提交
315 316
  def commit_authors
    @commit_authors ||= commits.authors
317 318 319
  end

  def authors
S
Stan Hu 已提交
320
    User.from_union([commit_authors, User.where(id: self.author_id)])
321 322
  end

323 324 325 326 327 328
  # 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

329
  def hook_attrs
330
    Gitlab::HookData::MergeRequestBuilder.new(self).build
331 332
  end

333 334 335 336 337 338 339 340
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee.try(:name)
    }
  end

341
  # These method are needed for compatibility with issues to not mess view and other code
342 343 344 345
  def assignees
    Array(assignee)
  end

346 347 348 349 350 351 352 353
  def assignee_ids
    Array(assignee_id)
  end

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

354 355 356 357
  def assignee_or_author?(user)
    author_id == user.id || assignee_id == user.id
  end

358
  # `from` argument can be a Namespace or Project.
359
  def to_reference(from = nil, full: false)
360 361
    reference = "#{self.class.reference_prefix}#{iid}"

362
    "#{project.to_reference(from, full: full)}#{reference}"
363 364
  end

365
  def commits
366 367 368 369 370 371 372 373 374
    return merge_request_diff.commits if persisted?

    commits_arr = if compare_commits
                    compare_commits.reverse
                  else
                    []
                  end

    CommitCollection.new(source_project, commits_arr, source_branch)
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
  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
391
      compare_commits.to_a.reverse.map(&:sha)
392
    else
393
      Array(diff_head_sha)
394 395 396
    end
  end

397 398 399 400 401 402 403 404 405
  # Returns true if there are commits that match at least one commit SHA.
  def includes_any_commits?(shas)
    if persisted?
      merge_request_diff.commits_by_shas(shas).exists?
    else
      (commit_shas & shas).present?
    end
  end

406
  def supports_suggestion?
407
    true
408 409
  end

410 411 412
  # Calls `MergeWorker` to proceed with the merge process and
  # updates `merge_jid` with the MergeWorker#jid.
  # This helps tracking enqueued and ongoing merge jobs.
413
  def merge_async(user_id, params)
414
    jid = MergeWorker.perform_async(id, user_id, params.to_h)
415 416 417
    update_column(:merge_jid, jid)
  end

L
lulalala 已提交
418 419 420 421 422 423 424 425 426 427
  def merge_participants
    participants = [author]

    if merge_when_pipeline_succeeds? && !participants.include?(merge_user)
      participants << merge_user
    end

    participants
  end

428 429
  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
430
  end
431

432
  def raw_diffs(*args)
433
    merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
S
Sean McGivern 已提交
434 435
  end

436
  def diffs(diff_options = {})
437
    if compare
438
      # When saving MR diffs, `expanded` is implicitly added (because we need
439 440
      # to save the entire contents to the DB), so add that here for
      # consistency.
441
      compare.diffs(diff_options.merge(expanded: true))
442
    else
443
      merge_request_diff.diffs(diff_options)
444
    end
S
Sean McGivern 已提交
445 446
  end

447 448 449 450
  def non_latest_diffs
    merge_request_diffs.where.not(id: merge_request_diff.id)
  end

451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
  def preloads_discussion_diff_highlighting?
    true
  end

  def preload_discussions_diff_highlight
    preloadable_files = note_diff_files.for_commit_or_unresolved

    discussions_diffs.load_highlight(preloadable_files.pluck(:id))
  end

  def discussions_diffs
    strong_memoize(:discussions_diffs) do
      Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
    end
  end

  def note_diff_files
    NoteDiffFile
      .where(diff_note: discussion_notes)
      .includes(diff_note: :project)
  end

J
Jacob Vosmaer 已提交
473
  def diff_size
474 475
    # Calling `merge_request_diff.diffs.real_size` will also perform
    # highlighting, which we don't need here.
476
    merge_request_diff&.real_size || diffs.real_size
J
Jacob Vosmaer 已提交
477 478
  end

479 480 481 482 483 484 485 486 487 488 489 490
  def modified_paths(past_merge_request_diff: nil)
    diffs = if past_merge_request_diff
              past_merge_request_diff
            elsif compare
              compare
            else
              self.merge_request_diff
            end

    diffs.modified_paths
  end

491
  def diff_base_commit
492
    if persisted?
493
      merge_request_diff.base_commit
494 495
    else
      branch_merge_base_commit
496 497 498 499 500 501 502 503
    end
  end

  def diff_start_commit
    if persisted?
      merge_request_diff.start_commit
    else
      target_branch_head
504 505 506
    end
  end

507 508 509 510 511 512 513 514 515
  def diff_head_commit
    if persisted?
      merge_request_diff.head_commit
    else
      source_branch_head
    end
  end

  def diff_start_sha
516 517 518 519 520
    if persisted?
      merge_request_diff.start_commit_sha
    else
      target_branch_head.try(:sha)
    end
521 522 523
  end

  def diff_base_sha
524 525 526 527 528
    if persisted?
      merge_request_diff.base_commit_sha
    else
      branch_merge_base_commit.try(:sha)
    end
529 530 531
  end

  def diff_head_sha
532 533 534 535 536
    if persisted?
      merge_request_diff.head_commit_sha
    else
      source_branch_head.try(:sha)
    end
537 538 539 540 541 542 543 544
  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

545 546 547 548 549 550 551 552 553 554 555 556 557 558
  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

559
  def source_branch_head
560 561 562 563 564
    strong_memoize(:source_branch_head) do
      if source_project && source_branch_ref
        source_project.repository.commit(source_branch_ref)
      end
    end
565 566 567
  end

  def target_branch_head
568 569 570
    strong_memoize(:target_branch_head) do
      target_project.repository.commit(target_branch_ref)
    end
571 572
  end

573 574 575 576 577 578 579 580 581
  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

582
  def target_branch_sha
583
    @target_branch_sha || target_branch_head.try(:sha)
584 585 586
  end

  def source_branch_sha
587
    @source_branch_sha || source_branch_head.try(:sha)
588 589
  end

590
  def diff_refs
591 592 593 594 595 596 597 598 599 600 601 602 603
    persisted? ? merge_request_diff.diff_refs : repository_diff_refs
  end

  # Instead trying to fetch the
  # persisted diff_refs, this method goes
  # straight to the repository to get the
  # most recent data possible.
  def repository_diff_refs
    Gitlab::Diff::DiffRefs.new(
      base_sha:  branch_merge_base_sha,
      start_sha: target_branch_sha,
      head_sha:  source_branch_sha
    )
604 605
  end

606 607 608 609
  def branch_merge_base_sha
    branch_merge_base_commit.try(:sha)
  end

610
  def validate_branches
611
    if target_project == source_project && target_branch == source_branch
612 613
      errors.add :branch_conflict, "You can't use same project/branch for source and target"
      return
614
    end
615

616
    if opened?
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
      similar_mrs = target_project
        .merge_requests
        .where(source_branch: source_branch, target_branch: target_branch)
        .where(source_project_id: source_project&.id)
        .opened

      similar_mrs = similar_mrs.where.not(id: id) if persisted?

      conflict = similar_mrs.first

      if conflict.present?
        errors.add(
          :validate_branches,
          "Another open merge request already exists for this source branch: #{conflict.to_reference}"
        )
632
      end
633
    end
634 635
  end

636 637 638 639 640 641
  def validate_target_project
    return true if target_project.merge_requests_enabled?

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

642
  def validate_fork
643
    return true unless target_project && source_project
644
    return true if target_project == source_project
645
    return true unless source_project_missing?
646

647
    errors.add :validate_fork,
648
               'Source project is not a fork of the target project'
649 650
  end

651
  def merge_ongoing?
652 653
    # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
    # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
654 655 656
    return true if locked?

    !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
657 658
  end

659
  def closed_without_fork?
660
    closed? && source_project_missing?
661 662
  end

663
  def source_project_missing?
664 665 666
    return false unless for_fork?
    return true unless source_project

667
    !source_project.in_fork_network_of?(target_project)
668 669
  end

670
  def reopenable?
671
    closed? && !source_project_missing? && source_branch_exists?
K
Katarzyna Kobierska 已提交
672 673
  end

674 675
  def ensure_merge_request_diff
    merge_request_diff || create_merge_request_diff
676 677
  end

678
  def create_merge_request_diff
679
    fetch_ref!
680

681 682 683 684 685
    # 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
686 687
  end

688 689 690 691
  def viewable_diffs
    @viewable_diffs ||= merge_request_diffs.viewable.to_a
  end

692
  def merge_request_diff_for(diff_refs_or_sha)
693 694 695 696 697 698 699 700 701 702
    matcher =
      if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
        {
          'start_commit_sha' => diff_refs_or_sha.start_sha,
          'head_commit_sha' => diff_refs_or_sha.head_sha,
          'base_commit_sha' => diff_refs_or_sha.base_sha
        }
      else
        { 'head_commit_sha' => diff_refs_or_sha }
      end
D
Douwe Maan 已提交
703

704 705 706
    viewable_diffs.find do |diff|
      diff.attributes.slice(*matcher.keys) == matcher
    end
707 708
  end

709 710 711 712 713 714 715 716 717 718 719
  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

720 721 722 723 724 725 726
  def clear_memoized_shas
    @target_branch_sha = @source_branch_sha = nil

    clear_memoization(:source_branch_head)
    clear_memoization(:target_branch_head)
  end

727
  def reload_diff_if_branch_changed
728 729
    if (source_branch_changed? || target_branch_changed?) &&
        (source_branch_head && target_branch_head)
730
      reload_diff
D
Dmitriy Zaporozhets 已提交
731 732 733
    end
  end

734
  # rubocop: disable CodeReuse/ServiceClass
735
  def reload_diff(current_user = nil)
736 737
    return unless open?

738
    MergeRequests::ReloadDiffsService.new(self, current_user).execute
739
  end
740
  # rubocop: enable CodeReuse/ServiceClass
741

742
  def check_if_can_be_merged
743
    return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
744

745
    can_be_merged =
746
      !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
747 748

    if can_be_merged
749 750 751 752
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
753 754
  end

D
Dmitriy Zaporozhets 已提交
755
  def merge_event
756
    @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
757 758
  end

759
  def closed_event
760
    @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
761 762
  end

763
  def work_in_progress?
T
Thomas Balthazar 已提交
764
    self.class.work_in_progress?(title)
765 766 767
  end

  def wipless_title
T
Thomas Balthazar 已提交
768 769 770 771 772
    self.class.wipless_title(self.title)
  end

  def wip_title
    self.class.wip_title(self.title)
773 774
  end

775 776
  def mergeable?(skip_ci_check: false)
    return false unless mergeable_state?(skip_ci_check: skip_ci_check)
777 778 779

    check_if_can_be_merged

780
    can_be_merged? && !should_be_rebased?
781 782
  end

783
  def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
784 785 786
    return false unless open?
    return false if work_in_progress?
    return false if broken?
787
    return false unless skip_ci_check || mergeable_ci_state?
788
    return false unless skip_discussions_check || mergeable_discussions_state?
789 790

    true
791 792
  end

793 794 795 796 797 798 799 800 801 802
  def mergeable_to_ref?
    return false if merged?
    return false if broken?

    # Given the `merge_ref_path` will have the same
    # state the `target_branch` would have. Ideally
    # we need to check if it can be merged to it.
    project.repository.can_be_merged?(diff_head_sha, target_branch)
  end

803 804 805 806 807 808 809 810
  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 已提交
811
  def can_cancel_merge_when_pipeline_succeeds?(current_user)
812
    can_be_merged_by?(current_user) || self.author == current_user
813 814
  end

815
  def can_remove_source_branch?(current_user)
816
    !ProtectedBranch.protected?(source_project, source_branch) &&
817
      !source_project.root_ref?(source_branch) &&
H
http://jneen.net/ 已提交
818
      Ability.allowed?(current_user, :push_code, source_project) &&
819
      diff_head_sha == source_branch_head.try(:sha)
820 821
  end

822
  def should_remove_source_branch?
823
    Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
824 825 826
  end

  def force_remove_source_branch?
827
    Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
828 829 830 831 832 833
  end

  def remove_source_branch?
    should_remove_source_branch? || force_remove_source_branch?
  end

834
  def notify_conflict?
835 836 837 838 839 840 841 842
    (opened? || locked?) &&
      has_commits? &&
      !branch_missing? &&
      !project.repository.can_be_merged?(diff_head_sha, target_branch)
  rescue Gitlab::Git::CommandError
    # Checking mergeability can trigger exception, e.g. non-utf8
    # We ignore this type of errors.
    false
843 844
  end

845
  def related_notes
846 847
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
848
    commit_ids = commit_shas.take(commits_for_notes_limit)
849

850 851 852
    commit_notes = Note
      .except(:order)
      .where(project_id: [source_project_id, target_project_id])
853
      .for_commit_id(commit_ids)
854 855 856 857 858

    # 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).
859 860
    Note
      .from_union([notes, commit_notes], remove_duplicates: false)
861
      .includes(:noteable)
862
  end
863

864
  alias_method :discussion_notes, :related_notes
865

866 867 868
  def mergeable_discussions_state?
    return true unless project.only_allow_merge_if_all_discussions_are_resolved?

869
    !discussions_to_be_resolved?
870 871
  end

I
Izaak Alpert 已提交
872 873 874 875
  def for_fork?
    target_project != source_project
  end

876 877 878 879
  # 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.
880
  # This optimization does not apply to issues from external sources.
881
  def cache_merge_request_closes_issues!(current_user = self.author)
882
    return unless project.issues_enabled?
883
    return if closed? || merged?
884

885
    transaction do
886
      self.merge_requests_closing_issues.delete_all
887

888
      closes_issues(current_user).each do |issue|
889 890
        next if issue.is_a?(ExternalIssue)

891
        self.merge_requests_closing_issues.create!(issue: issue)
892 893 894 895
      end
    end
  end

896 897 898 899 900 901 902 903 904 905 906 907
  def visible_closing_issues_for(current_user = self.author)
    strong_memoize(:visible_closing_issues_for) do
      if self.target_project.has_external_issue_tracker?
        closes_issues(current_user)
      else
        cached_closes_issues.select do |issue|
          Ability.allowed?(current_user, :read_issue, issue)
        end
      end
    end
  end

908
  # Return the set of issues that will be closed if this merge request is accepted.
909
  def closes_issues(current_user = self.author)
910
    if target_branch == project.default_branch
911
      messages = [title, description]
912
      messages.concat(commits.map(&:safe_message)) if merge_request_diff
913

914 915
      Gitlab::ClosingIssueExtractor.new(project, current_user)
        .closed_by_message(messages.join("\n"))
916 917 918 919 920
    else
      []
    end
  end

921
  def issues_mentioned_but_not_closing(current_user)
922
    return [] unless target_branch == project.default_branch
923

924
    ext = Gitlab::ReferenceExtractor.new(project, current_user)
925
    ext.analyze("#{title}\n#{description}")
926

927
    ext.issues - visible_closing_issues_for(current_user)
928 929
  end

930 931
  def target_project_path
    if target_project
932
      target_project.full_path
933 934 935 936 937 938 939
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
940
      source_project.full_path
941 942 943 944 945
    else
      "(removed)"
    end
  end

946 947
  def source_project_namespace
    if source_project && source_project.namespace
948
      source_project.namespace.full_path
949 950 951 952 953
    else
      "(removed)"
    end
  end

954 955
  def target_project_namespace
    if target_project && target_project.namespace
956
      target_project.namespace.full_path
957 958 959 960 961
    else
      "(removed)"
    end
  end

962 963 964
  def source_branch_exists?
    return false unless self.source_project

965
    self.source_project.repository.branch_exists?(self.source_branch)
966 967 968 969 970
  end

  def target_branch_exists?
    return false unless self.target_project

971
    self.target_project.repository.branch_exists?(self.target_branch)
972 973
  end

974
  def default_merge_commit_message(include_description: false)
975
    closes_issues_references = visible_closing_issues_for.map do |issue|
976 977 978
      issue.to_reference(target_project)
    end

979 980 981 982
    message = [
      "Merge branch '#{source_branch}' into '#{target_branch}'",
      title
    ]
983

984
    if !include_description && closes_issues_references.present?
985
      message << "Closes #{closes_issues_references.to_sentence}"
986
    end
987

988
    message << "#{description}" if include_description && description.present?
989
    message << "See merge request #{to_reference(full: true)}"
990

991
    message.join("\n\n")
992
  end
993

994 995 996 997 998 999 1000
  # Returns the oldest multi-line commit message, or the MR title if none found
  def default_squash_commit_message
    strong_memoize(:default_squash_commit_message) do
      commits.without_merge_commits.reverse.find(&:description?)&.safe_message || title
    end
  end

J
James Lopez 已提交
1001 1002
  def reset_merge_when_pipeline_succeeds
    return unless merge_when_pipeline_succeeds?
1003

J
James Lopez 已提交
1004
    self.merge_when_pipeline_succeeds = false
Z
Zeger-Jan van de Weg 已提交
1005
    self.merge_user = nil
1006 1007 1008
    if merge_params
      merge_params.delete('should_remove_source_branch')
      merge_params.delete('commit_message')
1009
      merge_params.delete('squash_commit_message')
1010
    end
Z
Zeger-Jan van de Weg 已提交
1011 1012 1013 1014

    self.save
  end

1015
  # Return array of possible target branches
S
Steven Burgart 已提交
1016
  # depends on target project of MR
1017 1018 1019 1020 1021 1022 1023 1024 1025
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
S
Steven Burgart 已提交
1026
  # depends on source project of MR
1027 1028 1029 1030 1031 1032 1033
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
1034

1035
  def has_ci?
1036
    return false if has_no_commits?
1037

1038
    !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
1039 1040 1041 1042 1043
  end

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

1045
  def broken?
1046
    has_no_commits? || branch_missing? || cannot_be_merged?
1047 1048
  end

1049
  def can_be_merged_by?(user)
1050
    access = ::Gitlab::UserAccess.new(user, project: project)
1051
    access.can_update_branch?(target_branch)
1052 1053 1054 1055 1056
  end

  def can_be_merged_via_command_line_by?(user)
    access = ::Gitlab::UserAccess.new(user, project: project)
    access.can_push_to_branch?(target_branch)
1057 1058
  end

1059
  def mergeable_ci_state?
J
James Lopez 已提交
1060
    return true unless project.only_allow_merge_if_pipeline_succeeds?
1061
    return true unless head_pipeline
1062

1063
    actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
1064 1065
  end

D
Douwe Maan 已提交
1066
  def environments_for(current_user)
1067
    return [] unless diff_head_commit
1068

D
Douwe Maan 已提交
1069 1070 1071
    @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
1072

D
Douwe Maan 已提交
1073 1074 1075 1076
      if source_project
        envs.concat EnvironmentsFinder.new(source_project, current_user,
          ref: source_branch, commit: diff_head_commit).execute
      end
1077

D
Douwe Maan 已提交
1078
      h[current_user] = envs.uniq
1079
    end
D
Douwe Maan 已提交
1080 1081

    @environments[current_user]
1082 1083
  end

1084 1085 1086 1087 1088 1089 1090 1091 1092
  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
1093

1094 1095
  def state_icon_name
    if merged?
E
Eric Eastwood 已提交
1096
      "git-merge"
1097
    elsif closed?
E
Eric Eastwood 已提交
1098
      "close"
1099
    else
E
Eric Eastwood 已提交
1100
      "issue-open-m"
1101 1102 1103
    end
  end

1104 1105
  def fetch_ref!
    target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
1106 1107
  end

1108
  def ref_path
1109
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
1110 1111
  end

1112 1113 1114 1115
  def merge_ref_path
    "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge"
  end

1116 1117 1118 1119 1120
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
1121
      unlock_mr
1122 1123
    end
  end
1124

1125 1126 1127
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

1128
    if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
1129
      cache = {
1130 1131
        source_sha: source_branch_sha,
        target_sha: target_branch_sha,
1132 1133 1134 1135 1136 1137 1138 1139 1140
        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
1141
    return 0 unless source_branch_sha && target_branch_sha
1142

L
Lin Jen-Shin 已提交
1143 1144
    target_project.repository
      .count_commits_between(source_branch_sha, target_branch_sha)
1145
  end
1146
  private :compute_diverged_commits_count
1147 1148 1149 1150 1151

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

S
Shinya Maeda 已提交
1152
  def all_pipelines(shas: all_commit_shas)
1153
    return Ci::Pipeline.none unless source_project
1154

1155 1156 1157 1158 1159 1160
    @all_pipelines ||=
      source_project.ci_pipelines
                    .for_merge_request(self, source_branch, all_commit_shas)
  end

  def update_head_pipeline
1161 1162 1163 1164
    find_actual_head_pipeline.try do |pipeline|
      self.head_pipeline = pipeline
      update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed?
    end
S
Shinya Maeda 已提交
1165 1166 1167 1168
  end

  def merge_request_pipeline_exists?
    merge_request_pipelines.exists?(sha: diff_head_sha)
1169
  end
1170

S
Shinya Maeda 已提交
1171 1172 1173 1174
  def has_test_reports?
    actual_head_pipeline&.has_test_reports?
  end

1175 1176 1177 1178
  def predefined_variables
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
      variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s)
1179 1180 1181 1182 1183 1184 1185 1186 1187 1188
      variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', value: ref_path.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', value: project.id.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path)
      variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
      variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
      variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
      variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
      variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
      variables.concat(source_project_variables)
1189 1190 1191
    end
  end

S
Shinya Maeda 已提交
1192
  def compare_test_reports
1193 1194
    unless has_test_reports?
      return { status: :error, status_reason: 'This merge request does not have test reports' }
S
Shinya Maeda 已提交
1195 1196
    end

G
Gilbert Roulot 已提交
1197 1198 1199 1200 1201 1202
    compare_reports(Ci::CompareTestReportsService)
  end

  def compare_reports(service_class)
    with_reactive_cache(service_class.name) do |data|
      unless service_class.new(project)
S
Shinya Maeda 已提交
1203 1204 1205 1206 1207 1208
        .latest?(base_pipeline, actual_head_pipeline, data)
        raise InvalidateReactiveCache
      end

      data
    end || { status: :parsing }
S
Shinya Maeda 已提交
1209 1210
  end

1211
  def calculate_reactive_cache(identifier, *args)
G
Gilbert Roulot 已提交
1212 1213 1214 1215 1216
    service_class = identifier.constantize

    raise NameError, service_class unless service_class < Ci::CompareReportsBaseService

    service_class.new(project).execute(base_pipeline, actual_head_pipeline)
S
Shinya Maeda 已提交
1217 1218
  end

1219
  def all_commits
1220
    # MySQL doesn't support LIMIT in a subquery.
M
micael.bergeron 已提交
1221 1222 1223 1224 1225
    diffs_relation = if Gitlab::Database.postgresql?
                       merge_request_diffs.recent
                     else
                       merge_request_diffs
                     end
1226

1227 1228 1229
    MergeRequestDiffCommit
      .where(merge_request_diff: diffs_relation)
      .limit(10_000)
1230 1231 1232 1233 1234 1235 1236
  end

  # Note that this could also return SHA from now dangling commits
  #
  def all_commit_shas
    @all_commit_shas ||= begin
      return commit_shas unless persisted?
M
micael.bergeron 已提交
1237

1238 1239
      all_commits.pluck(:sha).uniq
    end
1240 1241
  end

1242 1243 1244 1245
  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

1246 1247 1248 1249
  def short_merge_commit_sha
    Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
  end

1250
  def can_be_reverted?(current_user)
1251
    return false unless merge_commit
1252
    return false unless merged_at
1253

1254 1255 1256 1257 1258
    # It is not guaranteed that Note#created_at will be strictly later than
    # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
    # comparison, as will a HA environment if clocks are not *precisely*
    # synchronized. Add a minute's leeway to compensate for both possibilities
    cutoff = merged_at - 1.minute
1259

1260
    notes_association = notes_with_associations.where('created_at >= ?', cutoff)
1261 1262

    !merge_commit.has_been_reverted?(current_user, notes_association)
1263
  end
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273

  def merged_at
    strong_memoize(:merged_at) do
      next unless merged?

      metrics&.merged_at ||
        merge_event&.created_at ||
        notes.system.reorder(nil).find_by(note: 'merged')&.created_at
    end
  end
1274 1275

  def can_be_cherry_picked?
F
Fatih Acet 已提交
1276
    merge_commit.present?
1277
  end
1278

1279
  def has_complete_diff_refs?
1280
    diff_refs && diff_refs.complete?
1281 1282
  end

1283
  # rubocop: disable CodeReuse/ServiceClass
1284
  def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
1285
    return unless has_complete_diff_refs?
1286 1287
    return if new_diff_refs == old_diff_refs

1288 1289
    active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
      discussion.active?(old_diff_refs)
1290
    end
1291
    return if active_diff_discussions.empty?
1292

1293
    paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
1294

1295
    service = Discussions::UpdateDiffPositionService.new(
1296
      self.project,
1297
      current_user,
1298 1299 1300 1301 1302
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: paths
    )

1303 1304
    active_diff_discussions.each do |discussion|
      service.execute(discussion)
1305
    end
1306 1307 1308 1309 1310 1311

    if project.resolve_outdated_diff_discussions?
      MergeRequests::ResolvedDiscussionNotificationService
        .new(project, current_user)
        .execute(self)
    end
1312
  end
1313
  # rubocop: enable CodeReuse/ServiceClass
1314

1315 1316 1317
  def keep_around_commit
    project.repository.keep_around(self.merge_commit_sha)
  end
1318

1319
  def has_commits?
1320
    merge_request_diff && commits_count > 0
1321 1322 1323 1324 1325
  end

  def has_no_commits?
    !has_commits?
  end
1326

1327
  def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
1328 1329 1330 1331 1332
    return false unless can_be_merged_by?(current_user)

    return true if autocomplete_precheck

    return false unless mergeable?(skip_ci_check: true)
1333
    return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
1334 1335 1336 1337
    return false if last_diff_sha != diff_head_sha

    true
  end
1338

S
Shinya Maeda 已提交
1339
  def base_pipeline
1340
    @base_pipeline ||= project.ci_pipelines
S
Shinya Maeda 已提交
1341
      .order(id: :desc)
1342
      .find_by(sha: diff_base_sha, ref: target_branch)
S
Shinya Maeda 已提交
1343 1344
  end

F
Felipe Artur 已提交
1345 1346 1347 1348
  def discussions_rendered_on_frontend?
    true
  end

1349
  # rubocop: disable CodeReuse/ServiceClass
1350 1351 1352
  def update_project_counter_caches
    Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
  end
1353
  # rubocop: enable CodeReuse/ServiceClass
1354

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

M
micael.bergeron 已提交
1358 1359
    project.merge_requests.merged.where(author_id: author_id).empty?
  end
1360

1361 1362 1363
  # TODO: remove once production database rename completes
  alias_attribute :allow_collaboration, :allow_maintainer_to_push

1364
  def allow_collaboration
1365
    collaborative_push_possible? && allow_maintainer_to_push
1366 1367
  end

1368
  alias_method :allow_collaboration?, :allow_collaboration
1369

1370
  def collaborative_push_possible?
1371 1372 1373 1374 1375 1376
    source_project.present? && for_fork? &&
      target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
      source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
      !ProtectedBranch.protected?(source_project, source_branch)
  end

1377 1378
  def can_allow_collaboration?(user)
    collaborative_push_possible? &&
1379 1380
      Ability.allowed?(user, :push_code, source_project)
  end
1381 1382 1383 1384 1385 1386 1387

  def squash_in_progress?
    # The source project can be deleted
    return false unless source_project

    source_project.repository.squash_in_progress?(id)
  end
1388 1389 1390 1391 1392 1393 1394

  private

  def find_actual_head_pipeline
    source_project&.ci_pipelines
                  &.latest_for_merge_request(self, source_branch, diff_head_sha)
  end
1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405

  def source_project_variables
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
      break variables unless source_project

      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', value: source_project.id.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path)
      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url)
      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s)
    end
  end
D
Dmitriy Zaporozhets 已提交
1406
end