8fa361b2d9
commit 610c02c305d9fb3c7d271883450a5fee8b0cf16f Merge: f2088edb26084f24dcef0
Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 23:01:38 2018 +0900 Merge branch 'master-ce' into improve-junit-support-be commit f2088edb26008e5791b7be86fc02fc470c881143 Merge: c67e1d32cac 339f47abec1 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 21:52:10 2018 +0900 Merge branch 'improve-junit-support-be' of gitlab.com:gitlab-org/gitlab-ce into improve-junit-support-be commit c67e1d32cac731b895e2f49a24ce0e1726b8196c Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 21:51:39 2018 +0900 Remove debuggable fixtures commit 339f47abec1d0ce815e6103a087902d71d8ff5be Merge: 7a33a9be724 96b748fbcc0 Author: Filipa Lacerda <filipa@gitlab.com> Date: Tue Aug 7 11:10:18 2018 +0100 Merge branch 'improve-junit-support-be' of https://gitlab.com/gitlab-org/gitlab-ce into improve-junit-support-be * 'improve-junit-support-be' of https://gitlab.com/gitlab-org/gitlab-ce: Add spec for latest Add spec for merge request Add spec for cache invalidation Add spec for pipeline Add spec Fix specs Support corrupted fixtures Add cache key to error message commit 7a33a9be724dbde79a24cec77658952ff2d2fa6c Author: Filipa Lacerda <filipa@gitlab.com> Date: Tue Aug 7 11:09:56 2018 +0100 Show resolved failures commit 96b748fbcc00a98a13aeb78f5d97de9cf25035b6 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 18:50:05 2018 +0900 Add spec for latest commit 0e8b024169b4b0ac272331117ac2fa821c4052f7 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 18:24:38 2018 +0900 Add spec for merge request commit 8690a699bc98394ad4deebdc91e6690758c5965e Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 18:18:55 2018 +0900 Add spec for cache invalidation commit 97678e1612826af409ca8a04b6c0dc830f7b66c6 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 18:01:03 2018 +0900 Add spec for pipeline commit 96c2a698af049e4026c68e74b1f41a265464b2b2 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 17:52:21 2018 +0900 Add spec commit 67bcbd25a3c13abb78ea43c0411f5aed417b87d0 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 17:39:02 2018 +0900 Fix specs commit d7d49def2023f85c07d2718b83f35c8849f65f05 Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 13:11:07 2018 +0900 Support corrupted fixtures commit d58dbbc17a7d954db22082615f5331c148c1061b Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 13:10:47 2018 +0900 Add cache key to error message commit f6f976216dc36333b5e05e3f0acdfca689350483 Author: Shinya Maeda <shinya@gitlab.com> Date: Thu Aug 2 19:07:46 2018 +0900 bring back debaggable fixtures commit 48a7800e67a718145c0e88c324c0c1f9619e26a4 Merge: 2822b9e8a36dd627072b3
Author: Shinya Maeda <shinya@gitlab.com> Date: Tue Aug 7 09:22:22 2018 +0900 Merge branch 'master-ce' into improve-junit-support-be commit 2822b9e8a369162d098a72a58803c8494b2343cd Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 22:54:47 2018 +0900 Move cache invalidation policy to outside of reactive cache commit b35efb1764ae61bb31dacbf79dbc022dcee3a203 Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 22:34:10 2018 +0900 ADd reactive cache an ability to invalite cache commit feafee6f8a50f4a32866d8ae768e99766b0b7c73 Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 19:25:22 2018 +0900 Pipeline has test reports if latest builds have any commit f302dbb73abe03c6c431e7d52d526e28a1586fee Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 19:18:56 2018 +0900 Invalidate test reports cache if it's outdated commit 83adaca01a1ee1cd64cac86b6fa3d10e2e4e2b98 Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 18:08:06 2018 +0900 Revert cache invalidation in expire pipeline cache worker commit ec3af5de4ca34e2e92ad6b97f29733d6c65062bc Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 15:13:47 2018 +0900 Fix feature spec commit 0db48805a1ba68763be0504eb57218bde2380e4b Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 14:07:46 2018 +0900 Change lifetime of test reports cache to 10 minutes commit 17f7e78bfe2188c349cda1ff90a3ea94d337461e Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 14:07:19 2018 +0900 Add changelog commit 89c87585ab7f5333a8139c02b330dd2caf0be31a Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 14:01:20 2018 +0900 Add feature spec commit 1120cfd7a9ab06105f2e763c375fab00922b7e0c Author: Shinya Maeda <shinya@gitlab.com> Date: Mon Aug 6 12:55:47 2018 +0900 Invalidate cache when pipeline status transits. Correct test reports from the latest builds
1229 lines
34 KiB
Ruby
1229 lines
34 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class MergeRequest < ActiveRecord::Base
|
|
include AtomicInternalId
|
|
include IidRoutes
|
|
include Issuable
|
|
include Noteable
|
|
include Referable
|
|
include IgnorableColumn
|
|
include TimeTrackable
|
|
include ManualInverseAssociation
|
|
include EachBatch
|
|
include ThrottledTouch
|
|
include Gitlab::Utils::StrongMemoize
|
|
include LabelEventable
|
|
include ReactiveCaching
|
|
|
|
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
|
|
self.reactive_cache_refresh_interval = 10.minutes
|
|
self.reactive_cache_lifetime = 10.minutes
|
|
|
|
ignore_column :locked_at,
|
|
:ref_fetched,
|
|
:deleted_at
|
|
|
|
belongs_to :target_project, class_name: "Project"
|
|
belongs_to :source_project, class_name: "Project"
|
|
belongs_to :merge_user, class_name: "User"
|
|
|
|
has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
|
|
|
|
has_many :merge_request_diffs
|
|
|
|
has_one :merge_request_diff,
|
|
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
|
|
|
|
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
|
|
manual_inverse_association :latest_merge_request_diff, :merge_request
|
|
|
|
# This is the same as latest_merge_request_diff unless:
|
|
# 1. There are arguments - in which case we might be trying to force-reload.
|
|
# 2. This association is already loaded.
|
|
# 3. The latest diff does not exist.
|
|
#
|
|
# The second one in particular is important - MergeRequestDiff#merge_request
|
|
# is the inverse of MergeRequest#merge_request_diff, which means it may not be
|
|
# the latest diff, because we could have loaded any diff from this particular
|
|
# MR. If we haven't already loaded a diff, then it's fine to load the latest.
|
|
def merge_request_diff(*args)
|
|
fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?
|
|
|
|
fallback || super
|
|
end
|
|
|
|
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
|
|
|
|
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
|
|
has_many :merge_requests_closing_issues,
|
|
class_name: 'MergeRequestsClosingIssues',
|
|
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
|
|
|
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
|
|
|
|
belongs_to :assignee, class_name: "User"
|
|
|
|
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
|
|
|
|
after_create :ensure_merge_request_diff, unless: :importing?
|
|
after_update :clear_memoized_shas
|
|
after_update :reload_diff_if_branch_changed
|
|
after_save :ensure_metrics
|
|
|
|
# When this attribute is true some MR validation is ignored
|
|
# It allows us to close or modify broken merge requests
|
|
attr_accessor :allow_broken
|
|
|
|
# Temporary fields to store compare vars
|
|
# when creating new merge request
|
|
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
|
|
|
|
state_machine :state, initial: :opened do
|
|
event :close do
|
|
transition [:opened] => :closed
|
|
end
|
|
|
|
event :mark_as_merged do
|
|
transition [:opened, :locked] => :merged
|
|
end
|
|
|
|
event :reopen do
|
|
transition closed: :opened
|
|
end
|
|
|
|
event :lock_mr do
|
|
transition [:opened] => :locked
|
|
end
|
|
|
|
event :unlock_mr do
|
|
transition locked: :opened
|
|
end
|
|
|
|
before_transition any => :opened do |merge_request|
|
|
merge_request.merge_jid = nil
|
|
|
|
merge_request.run_after_commit do
|
|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
|
|
end
|
|
end
|
|
|
|
state :opened
|
|
state :closed
|
|
state :merged
|
|
state :locked
|
|
end
|
|
|
|
state_machine :merge_status, initial: :unchecked do
|
|
event :mark_as_unchecked do
|
|
transition [:can_be_merged, :unchecked] => :unchecked
|
|
transition [:cannot_be_merged, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
|
|
end
|
|
|
|
event :mark_as_mergeable do
|
|
transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged
|
|
end
|
|
|
|
event :mark_as_unmergeable do
|
|
transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged
|
|
end
|
|
|
|
state :unchecked
|
|
state :cannot_be_merged_recheck
|
|
state :can_be_merged
|
|
state :cannot_be_merged
|
|
|
|
around_transition do |merge_request, transition, block|
|
|
Gitlab::Timeless.timeless(merge_request, &block)
|
|
end
|
|
|
|
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
|
|
if merge_request.notify_conflict?
|
|
NotificationService.new.merge_request_unmergeable(merge_request)
|
|
TodoService.new.merge_request_became_unmergeable(merge_request)
|
|
end
|
|
end
|
|
|
|
def check_state?(merge_status)
|
|
[:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
|
|
end
|
|
end
|
|
|
|
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
|
validates :source_branch, presence: true
|
|
validates :target_project, presence: true
|
|
validates :target_branch, presence: true
|
|
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
|
|
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
|
validate :validate_fork, unless: :closed_without_fork?
|
|
validate :validate_target_project, on: :create
|
|
|
|
scope :by_source_or_target_branch, ->(branch_name) do
|
|
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
|
|
end
|
|
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
|
|
scope :of_projects, ->(ids) { where(target_project_id: ids) }
|
|
scope :from_project, ->(project) { where(source_project_id: project.id) }
|
|
scope :merged, -> { with_state(:merged) }
|
|
scope :closed_and_merged, -> { with_states(:closed, :merged) }
|
|
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
|
|
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
|
|
scope :join_project, -> { joins(:target_project) }
|
|
scope :references_project, -> { references(:target_project) }
|
|
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
|
|
|
|
after_save :keep_around_commit
|
|
|
|
def self.reference_prefix
|
|
'!'
|
|
end
|
|
|
|
def rebase_in_progress?
|
|
strong_memoize(:rebase_in_progress) do
|
|
# The source project can be deleted
|
|
next false unless source_project
|
|
|
|
source_project.repository.rebase_in_progress?(id)
|
|
end
|
|
end
|
|
|
|
# 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
|
|
def actual_head_pipeline
|
|
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
|
|
end
|
|
|
|
# Pattern used to extract `!123` merge request references from text
|
|
#
|
|
# This pattern supports cross-project references.
|
|
def self.reference_pattern
|
|
@reference_pattern ||= %r{
|
|
(#{Project.reference_pattern})?
|
|
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
|
|
}x
|
|
end
|
|
|
|
def self.link_reference_pattern
|
|
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
|
|
end
|
|
|
|
def self.reference_valid?(reference)
|
|
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
|
|
end
|
|
|
|
def self.project_foreign_key
|
|
'target_project_id'
|
|
end
|
|
|
|
# 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)
|
|
# 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)
|
|
union = Gitlab::SQL::Union.new([source, target])
|
|
|
|
where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
|
|
end
|
|
|
|
# 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
|
|
|
|
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
|
|
|
|
# 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
|
|
|
|
def hook_attrs
|
|
Gitlab::HookData::MergeRequestBuilder.new(self).build
|
|
end
|
|
|
|
# Returns a Hash of attributes to be used for Twitter card metadata
|
|
def card_attributes
|
|
{
|
|
'Author' => author.try(:name),
|
|
'Assignee' => assignee.try(:name)
|
|
}
|
|
end
|
|
|
|
# These method are needed for compatibility with issues to not mess view and other code
|
|
def assignees
|
|
Array(assignee)
|
|
end
|
|
|
|
def assignee_ids
|
|
Array(assignee_id)
|
|
end
|
|
|
|
def assignee_ids=(ids)
|
|
write_attribute(:assignee_id, ids.last)
|
|
end
|
|
|
|
def assignee_or_author?(user)
|
|
author_id == user.id || assignee_id == user.id
|
|
end
|
|
|
|
# `from` argument can be a Namespace or Project.
|
|
def to_reference(from = nil, full: false)
|
|
reference = "#{self.class.reference_prefix}#{iid}"
|
|
|
|
"#{project.to_reference(from, full: full)}#{reference}"
|
|
end
|
|
|
|
def commits
|
|
if persisted?
|
|
merge_request_diff.commits
|
|
elsif compare_commits
|
|
compare_commits.reverse
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def commits_count
|
|
if persisted?
|
|
merge_request_diff.commits_count
|
|
elsif compare_commits
|
|
compare_commits.size
|
|
else
|
|
0
|
|
end
|
|
end
|
|
|
|
def commit_shas
|
|
if persisted?
|
|
merge_request_diff.commit_shas
|
|
elsif compare_commits
|
|
compare_commits.to_a.reverse.map(&:sha)
|
|
else
|
|
Array(diff_head_sha)
|
|
end
|
|
end
|
|
|
|
# Calls `MergeWorker` to proceed with the merge process and
|
|
# updates `merge_jid` with the MergeWorker#jid.
|
|
# This helps tracking enqueued and ongoing merge jobs.
|
|
def merge_async(user_id, params)
|
|
jid = MergeWorker.perform_async(id, user_id, params.to_h)
|
|
update_column(:merge_jid, jid)
|
|
end
|
|
|
|
def merge_participants
|
|
participants = [author]
|
|
|
|
if merge_when_pipeline_succeeds? && !participants.include?(merge_user)
|
|
participants << merge_user
|
|
end
|
|
|
|
participants
|
|
end
|
|
|
|
def first_commit
|
|
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
|
|
end
|
|
|
|
def raw_diffs(*args)
|
|
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
|
|
end
|
|
|
|
def diffs(diff_options = {})
|
|
if compare
|
|
# When saving MR diffs, `expanded` is implicitly added (because we need
|
|
# to save the entire contents to the DB), so add that here for
|
|
# consistency.
|
|
compare.diffs(diff_options.merge(expanded: true))
|
|
else
|
|
merge_request_diff.diffs(diff_options)
|
|
end
|
|
end
|
|
|
|
def non_latest_diffs
|
|
merge_request_diffs.where.not(id: merge_request_diff.id)
|
|
end
|
|
|
|
def diff_size
|
|
# Calling `merge_request_diff.diffs.real_size` will also perform
|
|
# highlighting, which we don't need here.
|
|
merge_request_diff&.real_size || diffs.real_size
|
|
end
|
|
|
|
def diff_base_commit
|
|
if persisted?
|
|
merge_request_diff.base_commit
|
|
else
|
|
branch_merge_base_commit
|
|
end
|
|
end
|
|
|
|
def diff_start_commit
|
|
if persisted?
|
|
merge_request_diff.start_commit
|
|
else
|
|
target_branch_head
|
|
end
|
|
end
|
|
|
|
def diff_head_commit
|
|
if persisted?
|
|
merge_request_diff.head_commit
|
|
else
|
|
source_branch_head
|
|
end
|
|
end
|
|
|
|
def diff_start_sha
|
|
if persisted?
|
|
merge_request_diff.start_commit_sha
|
|
else
|
|
target_branch_head.try(:sha)
|
|
end
|
|
end
|
|
|
|
def diff_base_sha
|
|
if persisted?
|
|
merge_request_diff.base_commit_sha
|
|
else
|
|
branch_merge_base_commit.try(:sha)
|
|
end
|
|
end
|
|
|
|
def diff_head_sha
|
|
if persisted?
|
|
merge_request_diff.head_commit_sha
|
|
else
|
|
source_branch_head.try(:sha)
|
|
end
|
|
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_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
|
|
|
|
def source_branch_head
|
|
strong_memoize(:source_branch_head) do
|
|
if source_project && source_branch_ref
|
|
source_project.repository.commit(source_branch_ref)
|
|
end
|
|
end
|
|
end
|
|
|
|
def target_branch_head
|
|
strong_memoize(:target_branch_head) do
|
|
target_project.repository.commit(target_branch_ref)
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
def target_branch_sha
|
|
@target_branch_sha || target_branch_head.try(:sha)
|
|
end
|
|
|
|
def source_branch_sha
|
|
@source_branch_sha || source_branch_head.try(:sha)
|
|
end
|
|
|
|
def diff_refs
|
|
if persisted?
|
|
merge_request_diff.diff_refs
|
|
else
|
|
Gitlab::Diff::DiffRefs.new(
|
|
base_sha: diff_base_sha,
|
|
start_sha: diff_start_sha,
|
|
head_sha: diff_head_sha
|
|
)
|
|
end
|
|
end
|
|
|
|
def branch_merge_base_sha
|
|
branch_merge_base_commit.try(:sha)
|
|
end
|
|
|
|
def validate_branches
|
|
if target_project == source_project && target_branch == source_branch
|
|
errors.add :branch_conflict, "You can not use same project/branch for source and target"
|
|
end
|
|
|
|
if opened?
|
|
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
|
|
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
|
|
if similar_mrs.any?
|
|
errors.add :validate_branches,
|
|
"Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def validate_target_project
|
|
return true if target_project.merge_requests_enabled?
|
|
|
|
errors.add :base, 'Target project has disabled merge requests'
|
|
end
|
|
|
|
def validate_fork
|
|
return true unless target_project && source_project
|
|
return true if target_project == source_project
|
|
return true unless source_project_missing?
|
|
|
|
errors.add :validate_fork,
|
|
'Source project is not a fork of the target project'
|
|
end
|
|
|
|
def merge_ongoing?
|
|
# While the MergeRequest is locked, it should present itself as 'merge ongoing'.
|
|
# The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
|
|
return true if locked?
|
|
|
|
!!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
|
|
end
|
|
|
|
def closed_without_fork?
|
|
closed? && source_project_missing?
|
|
end
|
|
|
|
def source_project_missing?
|
|
return false unless for_fork?
|
|
return true unless source_project
|
|
|
|
!source_project.in_fork_network_of?(target_project)
|
|
end
|
|
|
|
def reopenable?
|
|
closed? && !source_project_missing? && source_branch_exists?
|
|
end
|
|
|
|
def ensure_merge_request_diff
|
|
merge_request_diff || create_merge_request_diff
|
|
end
|
|
|
|
def create_merge_request_diff
|
|
fetch_ref!
|
|
|
|
# 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
|
|
end
|
|
|
|
def reload_merge_request_diff
|
|
merge_request_diff(true)
|
|
end
|
|
|
|
def viewable_diffs
|
|
@viewable_diffs ||= merge_request_diffs.viewable.to_a
|
|
end
|
|
|
|
def merge_request_diff_for(diff_refs_or_sha)
|
|
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
|
|
|
|
viewable_diffs.find do |diff|
|
|
diff.attributes.slice(*matcher.keys) == matcher
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
def clear_memoized_shas
|
|
@target_branch_sha = @source_branch_sha = nil
|
|
|
|
clear_memoization(:source_branch_head)
|
|
clear_memoization(:target_branch_head)
|
|
end
|
|
|
|
def reload_diff_if_branch_changed
|
|
if (source_branch_changed? || target_branch_changed?) &&
|
|
(source_branch_head && target_branch_head)
|
|
reload_diff
|
|
end
|
|
end
|
|
|
|
def reload_diff(current_user = nil)
|
|
return unless open?
|
|
|
|
MergeRequests::ReloadDiffsService.new(self, current_user).execute
|
|
end
|
|
|
|
def check_if_can_be_merged
|
|
return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
|
|
|
|
can_be_merged =
|
|
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
|
|
|
|
if can_be_merged
|
|
mark_as_mergeable
|
|
else
|
|
mark_as_unmergeable
|
|
end
|
|
end
|
|
|
|
def merge_event
|
|
@merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
|
|
end
|
|
|
|
def closed_event
|
|
@closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
|
|
end
|
|
|
|
def work_in_progress?
|
|
self.class.work_in_progress?(title)
|
|
end
|
|
|
|
def wipless_title
|
|
self.class.wipless_title(self.title)
|
|
end
|
|
|
|
def wip_title
|
|
self.class.wip_title(self.title)
|
|
end
|
|
|
|
def mergeable?(skip_ci_check: false)
|
|
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
|
|
|
|
check_if_can_be_merged
|
|
|
|
can_be_merged? && !should_be_rebased?
|
|
end
|
|
|
|
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
|
|
return false unless open?
|
|
return false if work_in_progress?
|
|
return false if broken?
|
|
return false unless skip_ci_check || mergeable_ci_state?
|
|
return false unless skip_discussions_check || mergeable_discussions_state?
|
|
|
|
true
|
|
end
|
|
|
|
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
|
|
|
|
def can_cancel_merge_when_pipeline_succeeds?(current_user)
|
|
can_be_merged_by?(current_user) || self.author == current_user
|
|
end
|
|
|
|
def can_remove_source_branch?(current_user)
|
|
!ProtectedBranch.protected?(source_project, source_branch) &&
|
|
!source_project.root_ref?(source_branch) &&
|
|
Ability.allowed?(current_user, :push_code, source_project) &&
|
|
diff_head_sha == source_branch_head.try(:sha)
|
|
end
|
|
|
|
def should_remove_source_branch?
|
|
Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
|
|
end
|
|
|
|
def force_remove_source_branch?
|
|
Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
|
|
end
|
|
|
|
def remove_source_branch?
|
|
should_remove_source_branch? || force_remove_source_branch?
|
|
end
|
|
|
|
def notify_conflict?
|
|
(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
|
|
end
|
|
|
|
def related_notes
|
|
# Fetch comments only from last 100 commits
|
|
commits_for_notes_limit = 100
|
|
commit_ids = commit_shas.take(commits_for_notes_limit)
|
|
|
|
commit_notes = Note
|
|
.except(:order)
|
|
.where(project_id: [source_project_id, target_project_id])
|
|
.for_commit_id(commit_ids)
|
|
|
|
# We're using a UNION ALL here since this results in better performance
|
|
# compared to using OR statements. We're using UNION ALL since the queries
|
|
# used won't produce any duplicates (e.g. a note for a commit can't also be
|
|
# a note for an MR).
|
|
union = Gitlab::SQL::Union
|
|
.new([notes, commit_notes], remove_duplicates: false)
|
|
.to_sql
|
|
|
|
Note.from("(#{union}) #{Note.table_name}")
|
|
.includes(:noteable)
|
|
end
|
|
|
|
alias_method :discussion_notes, :related_notes
|
|
|
|
def mergeable_discussions_state?
|
|
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
|
|
|
|
!discussions_to_be_resolved?
|
|
end
|
|
|
|
def for_fork?
|
|
target_project != source_project
|
|
end
|
|
|
|
def project
|
|
target_project
|
|
end
|
|
|
|
# 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.
|
|
# This optimization does not apply to issues from external sources.
|
|
def cache_merge_request_closes_issues!(current_user = self.author)
|
|
return unless project.issues_enabled?
|
|
return if closed? || merged?
|
|
|
|
transaction do
|
|
self.merge_requests_closing_issues.delete_all
|
|
|
|
closes_issues(current_user).each do |issue|
|
|
next if issue.is_a?(ExternalIssue)
|
|
|
|
self.merge_requests_closing_issues.create!(issue: issue)
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
# Return the set of issues that will be closed if this merge request is accepted.
|
|
def closes_issues(current_user = self.author)
|
|
if target_branch == project.default_branch
|
|
messages = [title, description]
|
|
messages.concat(commits.map(&:safe_message)) if merge_request_diff
|
|
|
|
Gitlab::ClosingIssueExtractor.new(project, current_user)
|
|
.closed_by_message(messages.join("\n"))
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def issues_mentioned_but_not_closing(current_user)
|
|
return [] unless target_branch == project.default_branch
|
|
|
|
ext = Gitlab::ReferenceExtractor.new(project, current_user)
|
|
ext.analyze("#{title}\n#{description}")
|
|
|
|
ext.issues - visible_closing_issues_for(current_user)
|
|
end
|
|
|
|
def target_project_path
|
|
if target_project
|
|
target_project.full_path
|
|
else
|
|
"(removed)"
|
|
end
|
|
end
|
|
|
|
def source_project_path
|
|
if source_project
|
|
source_project.full_path
|
|
else
|
|
"(removed)"
|
|
end
|
|
end
|
|
|
|
def source_project_namespace
|
|
if source_project && source_project.namespace
|
|
source_project.namespace.full_path
|
|
else
|
|
"(removed)"
|
|
end
|
|
end
|
|
|
|
def target_project_namespace
|
|
if target_project && target_project.namespace
|
|
target_project.namespace.full_path
|
|
else
|
|
"(removed)"
|
|
end
|
|
end
|
|
|
|
def source_branch_exists?
|
|
return false unless self.source_project
|
|
|
|
self.source_project.repository.branch_exists?(self.source_branch)
|
|
end
|
|
|
|
def target_branch_exists?
|
|
return false unless self.target_project
|
|
|
|
self.target_project.repository.branch_exists?(self.target_branch)
|
|
end
|
|
|
|
def merge_commit_message(include_description: false)
|
|
closes_issues_references = visible_closing_issues_for.map do |issue|
|
|
issue.to_reference(target_project)
|
|
end
|
|
|
|
message = [
|
|
"Merge branch '#{source_branch}' into '#{target_branch}'",
|
|
title
|
|
]
|
|
|
|
if !include_description && closes_issues_references.present?
|
|
message << "Closes #{closes_issues_references.to_sentence}"
|
|
end
|
|
|
|
message << "#{description}" if include_description && description.present?
|
|
message << "See merge request #{to_reference(full: true)}"
|
|
|
|
message.join("\n\n")
|
|
end
|
|
|
|
def reset_merge_when_pipeline_succeeds
|
|
return unless merge_when_pipeline_succeeds?
|
|
|
|
self.merge_when_pipeline_succeeds = false
|
|
self.merge_user = nil
|
|
if merge_params
|
|
merge_params.delete('should_remove_source_branch')
|
|
merge_params.delete('commit_message')
|
|
end
|
|
|
|
self.save
|
|
end
|
|
|
|
# Return array of possible target branches
|
|
# depends on target project of MR
|
|
def target_branches
|
|
if target_project.nil?
|
|
[]
|
|
else
|
|
target_project.repository.branch_names
|
|
end
|
|
end
|
|
|
|
# Return array of possible source branches
|
|
# depends on source project of MR
|
|
def source_branches
|
|
if source_project.nil?
|
|
[]
|
|
else
|
|
source_project.repository.branch_names
|
|
end
|
|
end
|
|
|
|
def has_ci?
|
|
return false if has_no_commits?
|
|
|
|
!!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
|
|
end
|
|
|
|
def branch_missing?
|
|
!source_branch_exists? || !target_branch_exists?
|
|
end
|
|
|
|
def broken?
|
|
has_no_commits? || branch_missing? || cannot_be_merged?
|
|
end
|
|
|
|
def can_be_merged_by?(user)
|
|
access = ::Gitlab::UserAccess.new(user, project: project)
|
|
access.can_update_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)
|
|
end
|
|
|
|
def mergeable_ci_state?
|
|
return true unless project.only_allow_merge_if_pipeline_succeeds?
|
|
return true unless head_pipeline
|
|
|
|
actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
|
|
end
|
|
|
|
def environments_for(current_user)
|
|
return [] unless diff_head_commit
|
|
|
|
@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
|
|
|
|
if source_project
|
|
envs.concat EnvironmentsFinder.new(source_project, current_user,
|
|
ref: source_branch, commit: diff_head_commit).execute
|
|
end
|
|
|
|
h[current_user] = envs.uniq
|
|
end
|
|
|
|
@environments[current_user]
|
|
end
|
|
|
|
def state_human_name
|
|
if merged?
|
|
"Merged"
|
|
elsif closed?
|
|
"Closed"
|
|
else
|
|
"Open"
|
|
end
|
|
end
|
|
|
|
def state_icon_name
|
|
if merged?
|
|
"git-merge"
|
|
elsif closed?
|
|
"close"
|
|
else
|
|
"issue-open-m"
|
|
end
|
|
end
|
|
|
|
def fetch_ref!
|
|
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
|
|
end
|
|
|
|
def ref_path
|
|
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
|
|
end
|
|
|
|
def in_locked_state
|
|
begin
|
|
lock_mr
|
|
yield
|
|
ensure
|
|
unlock_mr
|
|
end
|
|
end
|
|
|
|
def diverged_commits_count
|
|
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
|
|
|
|
if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
|
|
cache = {
|
|
source_sha: source_branch_sha,
|
|
target_sha: target_branch_sha,
|
|
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
|
|
return 0 unless source_branch_sha && target_branch_sha
|
|
|
|
target_project.repository
|
|
.count_commits_between(source_branch_sha, target_branch_sha)
|
|
end
|
|
private :compute_diverged_commits_count
|
|
|
|
def diverged_from_target_branch?
|
|
diverged_commits_count > 0
|
|
end
|
|
|
|
def all_pipelines
|
|
return Ci::Pipeline.none unless source_project
|
|
|
|
@all_pipelines ||= source_project.pipelines
|
|
.where(sha: all_commit_shas, ref: source_branch)
|
|
.order(id: :desc)
|
|
end
|
|
|
|
def has_test_reports?
|
|
actual_head_pipeline&.has_test_reports?
|
|
end
|
|
|
|
def compare_test_reports
|
|
unless has_test_reports?
|
|
return { status: :error, status_reason: 'This merge request does not have test reports' }
|
|
end
|
|
|
|
with_reactive_cache(:compare_test_results) do |data|
|
|
unless Ci::CompareTestReportsService.new(project)
|
|
.latest?(base_pipeline, actual_head_pipeline, data)
|
|
raise InvalidateReactiveCache
|
|
end
|
|
|
|
data
|
|
end || { status: :parsing }
|
|
end
|
|
|
|
def calculate_reactive_cache(identifier, *args)
|
|
case identifier.to_sym
|
|
when :compare_test_results
|
|
Ci::CompareTestReportsService.new(project).execute(
|
|
base_pipeline, actual_head_pipeline)
|
|
else
|
|
raise NotImplementedError, "Unknown identifier: #{identifier}"
|
|
end
|
|
end
|
|
|
|
def all_commits
|
|
# MySQL doesn't support LIMIT in a subquery.
|
|
diffs_relation = if Gitlab::Database.postgresql?
|
|
merge_request_diffs.recent
|
|
else
|
|
merge_request_diffs
|
|
end
|
|
|
|
MergeRequestDiffCommit
|
|
.where(merge_request_diff: diffs_relation)
|
|
.limit(10_000)
|
|
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?
|
|
|
|
all_commits.pluck(:sha).uniq
|
|
end
|
|
end
|
|
|
|
def merge_commit
|
|
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
|
|
end
|
|
|
|
def short_merge_commit_sha
|
|
Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
|
|
end
|
|
|
|
def can_be_reverted?(current_user)
|
|
return false unless merge_commit
|
|
return false unless merged_at
|
|
|
|
# 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
|
|
|
|
notes_association = notes_with_associations.where('created_at >= ?', cutoff)
|
|
|
|
!merge_commit.has_been_reverted?(current_user, notes_association)
|
|
end
|
|
|
|
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
|
|
|
|
def can_be_cherry_picked?
|
|
merge_commit.present?
|
|
end
|
|
|
|
def has_complete_diff_refs?
|
|
diff_refs && diff_refs.complete?
|
|
end
|
|
|
|
def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
|
|
return unless has_complete_diff_refs?
|
|
return if new_diff_refs == old_diff_refs
|
|
|
|
active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
|
|
discussion.active?(old_diff_refs)
|
|
end
|
|
return if active_diff_discussions.empty?
|
|
|
|
paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
|
|
|
|
service = Discussions::UpdateDiffPositionService.new(
|
|
self.project,
|
|
current_user,
|
|
old_diff_refs: old_diff_refs,
|
|
new_diff_refs: new_diff_refs,
|
|
paths: paths
|
|
)
|
|
|
|
active_diff_discussions.each do |discussion|
|
|
service.execute(discussion)
|
|
end
|
|
|
|
if project.resolve_outdated_diff_discussions?
|
|
MergeRequests::ResolvedDiscussionNotificationService
|
|
.new(project, current_user)
|
|
.execute(self)
|
|
end
|
|
end
|
|
|
|
def keep_around_commit
|
|
project.repository.keep_around(self.merge_commit_sha)
|
|
end
|
|
|
|
def has_commits?
|
|
merge_request_diff && commits_count > 0
|
|
end
|
|
|
|
def has_no_commits?
|
|
!has_commits?
|
|
end
|
|
|
|
def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
|
|
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 actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
|
|
return false if last_diff_sha != diff_head_sha
|
|
|
|
true
|
|
end
|
|
|
|
def base_pipeline
|
|
@base_pipeline ||= project.pipelines
|
|
.order(id: :desc)
|
|
.find_by(sha: diff_base_sha)
|
|
end
|
|
|
|
def discussions_rendered_on_frontend?
|
|
true
|
|
end
|
|
|
|
def update_project_counter_caches
|
|
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
|
|
end
|
|
|
|
def first_contribution?
|
|
return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
|
|
|
|
project.merge_requests.merged.where(author_id: author_id).empty?
|
|
end
|
|
|
|
# TODO: remove once production database rename completes
|
|
alias_attribute :allow_collaboration, :allow_maintainer_to_push
|
|
|
|
def allow_collaboration
|
|
collaborative_push_possible? && allow_maintainer_to_push
|
|
end
|
|
|
|
alias_method :allow_collaboration?, :allow_collaboration
|
|
|
|
def collaborative_push_possible?
|
|
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
|
|
|
|
def can_allow_collaboration?(user)
|
|
collaborative_push_possible? &&
|
|
Ability.allowed?(user, :push_code, source_project)
|
|
end
|
|
|
|
def squash_in_progress?
|
|
# The source project can be deleted
|
|
return false unless source_project
|
|
|
|
source_project.repository.squash_in_progress?(id)
|
|
end
|
|
end
|