2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2019-07-07 11:18:12 +05:30
class MergeRequest < ApplicationRecord
2018-10-15 14:42:47 +05:30
include AtomicInternalId
2018-11-08 19:23:39 +05:30
include IidRoutes
2015-09-11 14:41:01 +05:30
include Issuable
2017-08-17 22:00:37 +05:30
include Noteable
2015-09-11 14:41:01 +05:30
include Referable
2018-11-08 19:23:39 +05:30
include Presentable
2018-03-17 18:26:18 +05:30
include TimeTrackable
include ManualInverseAssociation
include EachBatch
include ThrottledTouch
include Gitlab :: Utils :: StrongMemoize
2018-11-18 11:00:15 +05:30
include LabelEventable
include ReactiveCaching
2018-12-05 23:21:45 +05:30
include FromUnion
2019-07-31 22:56:46 +05:30
include DeprecatedAssignee
2019-12-26 22:10:19 +05:30
include ShaAttribute
2020-01-01 13:55:28 +05:30
include IgnorableColumns
2020-04-08 14:13:33 +05:30
include MilestoneEventable
2020-05-24 23:13:21 +05:30
include StateEventable
2020-07-28 23:09:34 +05:30
include ApprovableBase
2020-11-24 15:15:51 +05:30
include IdInOrdered
2021-01-29 00:20:46 +05:30
include Todoable
2020-07-28 23:09:34 +05:30
extend :: Gitlab :: Utils :: Override
2019-12-26 22:10:19 +05:30
sha_attribute :squash_commit_sha
2020-11-24 15:15:51 +05:30
sha_attribute :merge_ref_sha
2018-11-18 11:00:15 +05:30
self . reactive_cache_key = - > ( model ) { [ model . project . id , model . iid ] }
self . reactive_cache_refresh_interval = 10 . minutes
self . reactive_cache_lifetime = 10 . minutes
2021-01-03 14:25:43 +05:30
self . reactive_cache_work_type = :no_dependency
2017-09-10 17:25:29 +05:30
2019-03-02 22:35:43 +05:30
SORTING_PREFERENCE_FIELD = :merge_requests_sort
2021-04-17 20:07:23 +05:30
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
2021-09-04 01:27:46 +05:30
'Ci::CompareMetricsReportsService' = > - > ( project ) { true } ,
2021-04-29 21:17:54 +05:30
'Ci::CompareCodequalityReportsService' = > - > ( project ) { true }
2021-04-17 20:07:23 +05:30
} . freeze
2017-08-17 22:00:37 +05:30
belongs_to :target_project , class_name : " Project "
belongs_to :source_project , class_name : " Project "
2015-12-23 02:04:40 +05:30
belongs_to :merge_user , class_name : " User "
2020-05-24 23:13:21 +05:30
belongs_to :iteration , foreign_key : 'sprint_id'
2014-09-02 18:07:02 +05:30
2021-01-29 00:20:46 +05:30
has_internal_id :iid , scope : :target_project , track_if : - > { ! importing? } ,
init : - > ( mr , scope ) do
if mr
mr . target_project & . merge_requests & . maximum ( :iid )
elsif scope [ :project ]
where ( target_project : scope [ :project ] ) . maximum ( :iid )
end
end
2018-10-15 14:42:47 +05:30
2021-03-11 19:13:27 +05:30
has_many :merge_request_diffs ,
- > { regular } , inverse_of : :merge_request
2020-10-24 23:57:45 +05:30
has_many :merge_request_context_commits , inverse_of : :merge_request
2020-03-13 15:44:24 +05:30
has_many :merge_request_context_commit_diff_files , through : :merge_request_context_commits , source : :diff_files
2018-03-17 18:26:18 +05:30
2016-09-29 09:46:39 +05:30
has_one :merge_request_diff ,
2021-03-11 19:13:27 +05:30
- > { regular . order ( 'merge_request_diffs.id DESC' ) } , inverse_of : :merge_request
has_one :merge_head_diff ,
- > { merge_head } , inverse_of : :merge_request , class_name : 'MergeRequestDiff'
2021-01-29 00:20:46 +05:30
has_one :cleanup_schedule , inverse_of : :merge_request
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
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.
2020-04-08 14:13:33 +05:30
# 4. It doesn't have any merge_request_diffs - it returns an empty MergeRequestDiff
2018-03-17 18:26:18 +05:30
#
# 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.
2019-02-15 15:39:39 +05:30
def merge_request_diff
fallback = latest_merge_request_diff unless association ( :merge_request_diff ) . loaded?
2018-03-17 18:26:18 +05:30
2020-04-08 14:13:33 +05:30
fallback || super || MergeRequestDiff . new ( merge_request_id : id )
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
belongs_to :head_pipeline , foreign_key : " head_pipeline_id " , class_name : " Ci::Pipeline "
2014-09-02 18:07:02 +05:30
2019-12-04 20:38:33 +05:30
has_many :events , as : :target , dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
has_many :merge_requests_closing_issues ,
class_name : 'MergeRequestsClosingIssues' ,
dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
2016-09-29 09:46:39 +05:30
2018-11-18 11:00:15 +05:30
has_many :cached_closes_issues , through : :merge_requests_closing_issues , source : :issue
2019-07-31 22:56:46 +05:30
has_many :pipelines_for_merge_request , foreign_key : 'merge_request_id' , class_name : 'Ci::Pipeline'
2019-07-07 11:18:12 +05:30
has_many :suggestions , through : :notes
2019-12-26 22:10:19 +05:30
has_many :unresolved_notes , - > { unresolved } , as : :noteable , class_name : 'Note'
2018-11-18 11:00:15 +05:30
2019-07-07 11:18:12 +05:30
has_many :merge_request_assignees
2019-07-31 22:56:46 +05:30
has_many :assignees , class_name : " User " , through : :merge_request_assignees
2020-11-24 15:15:51 +05:30
has_many :merge_request_reviewers
has_many :reviewers , class_name : " User " , through : :merge_request_reviewers
2020-03-13 15:44:24 +05:30
has_many :user_mentions , class_name : " MergeRequestUserMention " , dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
2020-01-01 13:55:28 +05:30
has_many :deployment_merge_requests
# These are deployments created after the merge request has been merged, and
# the merge request was tracked explicitly (instead of implicitly using a CI
# build).
has_many :deployments ,
through : :deployment_merge_requests
2017-08-17 22:00:37 +05:30
2020-06-23 00:09:42 +05:30
has_many :draft_notes
has_many :reviews , inverse_of : :merge_request
2019-10-31 01:37:42 +05:30
KNOWN_MERGE_PARAMS = [
:auto_merge_strategy ,
:should_remove_source_branch ,
:force_remove_source_branch ,
:commit_message ,
:squash_commit_message ,
:sha
] . freeze
2017-09-10 17:25:29 +05:30
serialize :merge_params , Hash # rubocop:disable Cop/ActiveRecordSerialize
2015-12-23 02:04:40 +05:30
2021-09-04 01:27:46 +05:30
before_validation :set_draft_status
2019-03-13 22:55:13 +05:30
after_create :ensure_merge_request_diff
2018-03-17 18:26:18 +05:30
after_update :clear_memoized_shas
2016-09-29 09:46:39 +05:30
after_update :reload_diff_if_branch_changed
2020-06-23 00:09:42 +05:30
after_commit :ensure_metrics , on : [ :create , :update ] , unless : :importing?
2020-03-13 15:44:24 +05:30
after_commit :expire_etag_cache , unless : :importing?
2014-09-02 18:07:02 +05:30
# 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
2016-11-03 12:29:30 +05:30
attr_accessor :can_be_created , :compare_commits , :diff_options , :compare
2016-06-02 11:05:42 +05:30
2021-01-03 14:25:43 +05:30
participant :reviewers
2019-12-21 20:55:43 +05:30
# Keep states definition to be evaluated before the state_machine block to avoid spec failures.
# If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil.
def self . available_state_names
super + [ :merged , :locked ]
end
2020-01-01 13:55:28 +05:30
state_machine :state_id , initial : :opened , initialize : false do
2014-09-02 18:07:02 +05:30
event :close do
2017-09-10 17:25:29 +05:30
transition [ :opened ] = > :closed
2014-09-02 18:07:02 +05:30
end
2015-09-25 12:07:36 +05:30
event :mark_as_merged do
2017-09-10 17:25:29 +05:30
transition [ :opened , :locked ] = > :merged
2014-09-02 18:07:02 +05:30
end
event :reopen do
2017-09-10 17:25:29 +05:30
transition closed : :opened
2014-09-02 18:07:02 +05:30
end
event :lock_mr do
2017-09-10 17:25:29 +05:30
transition [ :opened ] = > :locked
2014-09-02 18:07:02 +05:30
end
event :unlock_mr do
2017-09-10 17:25:29 +05:30
transition locked : :opened
2015-04-26 12:48:37 +05:30
end
2018-03-17 18:26:18 +05:30
before_transition any = > :opened do | merge_request |
merge_request . merge_jid = nil
2019-02-15 15:39:39 +05:30
end
2018-03-17 18:26:18 +05:30
2019-02-15 15:39:39 +05:30
after_transition any = > :opened do | merge_request |
2018-03-17 18:26:18 +05:30
merge_request . run_after_commit do
UpdateHeadPipelineForMergeRequestWorker . perform_async ( merge_request . id )
end
end
2019-12-21 20:55:43 +05:30
state :opened , value : MergeRequest . available_states [ :opened ]
state :closed , value : MergeRequest . available_states [ :closed ]
state :merged , value : MergeRequest . available_states [ :merged ]
state :locked , value : MergeRequest . available_states [ :locked ]
end
# Alias to state machine .with_state_id method
# This needs to be defined after the state machine block to avoid errors
class << self
alias_method :with_state , :with_state_id
alias_method :with_states , :with_state_ids
2014-09-02 18:07:02 +05:30
end
state_machine :merge_status , initial : :unchecked do
2021-04-17 20:07:23 +05:30
event :mark_as_preparing do
transition unchecked : :preparing
end
2014-09-02 18:07:02 +05:30
event :mark_as_unchecked do
2021-04-17 20:07:23 +05:30
transition [ :preparing , :can_be_merged , :checking ] = > :unchecked
transition [ :cannot_be_merged , :cannot_be_merged_rechecking ] = > :cannot_be_merged_recheck
2014-09-02 18:07:02 +05:30
end
2020-03-13 15:44:24 +05:30
event :mark_as_checking do
2020-04-22 19:07:51 +05:30
transition unchecked : :checking
transition cannot_be_merged_recheck : :cannot_be_merged_rechecking
2020-03-13 15:44:24 +05:30
end
2014-09-02 18:07:02 +05:30
event :mark_as_mergeable do
2020-04-22 19:07:51 +05:30
transition [ :unchecked , :cannot_be_merged_recheck , :checking , :cannot_be_merged_rechecking ] = > :can_be_merged
2014-09-02 18:07:02 +05:30
end
event :mark_as_unmergeable do
2020-04-22 19:07:51 +05:30
transition [ :unchecked , :cannot_be_merged_recheck , :checking , :cannot_be_merged_rechecking ] = > :cannot_be_merged
2014-09-02 18:07:02 +05:30
end
2021-04-17 20:07:23 +05:30
state :preparing
2014-09-02 18:07:02 +05:30
state :unchecked
2018-11-08 19:23:39 +05:30
state :cannot_be_merged_recheck
2020-03-13 15:44:24 +05:30
state :checking
2020-04-22 19:07:51 +05:30
state :cannot_be_merged_rechecking
2014-09-02 18:07:02 +05:30
state :can_be_merged
state :cannot_be_merged
2015-04-26 12:48:37 +05:30
around_transition do | merge_request , transition , block |
2016-08-24 12:49:21 +05:30
Gitlab :: Timeless . timeless ( merge_request , & block )
2015-04-26 12:48:37 +05:30
end
2018-11-08 19:23:39 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2020-04-22 19:07:51 +05:30
after_transition [ :unchecked , :checking ] = > :cannot_be_merged do | merge_request , transition |
2018-11-08 19:23:39 +05:30
if merge_request . notify_conflict?
NotificationService . new . merge_request_unmergeable ( merge_request )
TodoService . new . merge_request_became_unmergeable ( merge_request )
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-11-08 19:23:39 +05:30
def check_state? ( merge_status )
2020-04-25 10:58:03 +05:30
[ :unchecked , :cannot_be_merged_recheck , :checking , :cannot_be_merged_rechecking ] . include? ( merge_status . to_sym )
2018-11-08 19:23:39 +05:30
end
2014-09-02 18:07:02 +05:30
end
2020-04-22 19:07:51 +05:30
# Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking`
# to avoid exposing unnecessary internal state
def public_merge_status
2021-04-17 20:07:23 +05:30
cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status
2020-04-22 19:07:51 +05:30
end
2021-02-22 17:27:13 +05:30
validates :source_project , presence : true , unless : [ :allow_broken , :importing? , :closed_or_merged_without_fork? ]
2014-09-02 18:07:02 +05:30
validates :source_branch , presence : true
validates :target_project , presence : true
validates :target_branch , presence : true
2019-09-04 21:01:54 +05:30
validates :merge_user , presence : true , if : :auto_merge_enabled? , unless : :importing?
2021-02-22 17:27:13 +05:30
validate :validate_branches , unless : [ :allow_broken , :importing? , :closed_or_merged_without_fork? ]
validate :validate_fork , unless : :closed_or_merged_without_fork?
2017-08-17 22:00:37 +05:30
validate :validate_target_project , on : :create
2014-09-02 18:07:02 +05:30
2017-08-17 22:00:37 +05:30
scope :by_source_or_target_branch , - > ( branch_name ) do
where ( " source_branch = :branch OR target_branch = :branch " , branch : branch_name )
end
2014-09-02 18:07:02 +05:30
scope :by_milestone , - > ( milestone ) { where ( milestone_id : milestone ) }
scope :of_projects , - > ( ids ) { where ( target_project_id : ids ) }
2016-06-02 11:05:42 +05:30
scope :from_project , - > ( project ) { where ( source_project_id : project . id ) }
2021-09-04 01:27:46 +05:30
scope :from_fork , - > { where ( 'source_project_id <> target_project_id' ) }
2020-01-01 13:55:28 +05:30
scope :from_and_to_forks , - > ( project ) do
2021-09-04 01:27:46 +05:30
from_fork . where ( 'source_project_id = ? OR target_project_id = ?' , project . id , project . id )
2020-01-01 13:55:28 +05:30
end
2015-09-11 14:41:01 +05:30
scope :merged , - > { with_state ( :merged ) }
2019-12-04 20:38:33 +05:30
scope :open_and_closed , - > { with_states ( :opened , :closed ) }
2021-09-04 01:27:46 +05:30
scope :drafts , - > { where ( draft : true ) }
2016-09-13 17:45:13 +05:30
scope :from_source_branches , - > ( branches ) { where ( source_branch : branches ) }
2018-03-17 18:26:18 +05:30
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
2019-12-26 22:10:19 +05:30
scope :by_merge_commit_sha , - > ( sha ) do
where ( merge_commit_sha : sha )
end
2021-03-08 18:12:59 +05:30
scope :by_squash_commit_sha , - > ( sha ) do
where ( squash_commit_sha : sha )
end
2021-04-29 21:17:54 +05:30
scope :by_merge_or_squash_commit_sha , - > ( sha ) do
from_union ( [ by_squash_commit_sha ( sha ) , by_merge_commit_sha ( sha ) ] )
end
2021-03-08 18:12:59 +05:30
scope :by_related_commit_sha , - > ( sha ) do
from_union (
[
by_commit_sha ( sha ) ,
by_squash_commit_sha ( sha ) ,
by_merge_commit_sha ( sha )
2021-03-11 19:13:27 +05:30
]
2021-03-08 18:12:59 +05:30
)
end
2015-12-23 02:04:40 +05:30
scope :join_project , - > { joins ( :target_project ) }
2021-04-29 21:17:54 +05:30
scope :join_metrics , - > ( target_project_id = nil ) do
# Do not join the relation twice
return self if self . arel . join_sources . any? { | join | join . left . try ( :name ) . eql? ( MergeRequest :: Metrics . table_name ) }
2020-11-24 15:15:51 +05:30
query = joins ( :metrics )
2021-04-29 21:17:54 +05:30
2021-09-30 23:02:18 +05:30
if ! target_project_id && self . where_values_hash [ " target_project_id " ]
target_project_id = self . where_values_hash [ " target_project_id " ]
query = query . unscope ( where : :target_project_id )
end
2021-04-29 21:17:54 +05:30
project_condition = if target_project_id
MergeRequest :: Metrics . arel_table [ :target_project_id ] . eq ( target_project_id )
else
MergeRequest . arel_table [ :target_project_id ] . eq ( MergeRequest :: Metrics . arel_table [ :target_project_id ] )
end
query . where ( project_condition )
2020-11-24 15:15:51 +05:30
end
2015-12-23 02:04:40 +05:30
scope :references_project , - > { references ( :target_project ) }
2019-07-07 11:18:12 +05:30
scope :with_api_entity_associations , - > {
2020-10-24 23:57:45 +05:30
preload_routables
. preload ( :assignees , :author , :unresolved_notes , :labels , :milestone ,
2021-02-22 17:27:13 +05:30
:timelogs , :latest_merge_request_diff , :reviewers ,
2020-10-24 23:57:45 +05:30
target_project : :project_feature ,
metrics : [ :latest_closed_by , :merged_by ] )
2019-07-07 11:18:12 +05:30
}
2020-07-28 23:09:34 +05:30
2021-01-03 14:25:43 +05:30
scope :with_csv_entity_associations , - > { preload ( :assignees , :approved_by_users , :author , :milestone , metrics : [ :merged_by ] ) }
2021-04-29 21:17:54 +05:30
scope :with_jira_integration_associations , - > { preload_routables . preload ( :metrics , :assignees , :author ) }
2021-01-03 14:25:43 +05:30
2019-12-04 20:38:33 +05:30
scope :by_target_branch_wildcard , - > ( wildcard_branch_name ) do
where ( " target_branch LIKE ? " , ApplicationRecord . sanitize_sql_like ( wildcard_branch_name ) . tr ( '*' , '%' ) )
end
scope :by_target_branch , - > ( branch_name ) { where ( target_branch : branch_name ) }
2021-10-27 15:23:28 +05:30
scope :order_by_metric , - > ( metric , direction ) do
2022-06-21 17:19:12 +05:30
column_expression = MergeRequest :: Metrics . arel_table [ metric ]
column_expression_with_direction = direction == 'ASC' ? column_expression . asc : column_expression . desc
2021-04-17 20:07:23 +05:30
order = Gitlab :: Pagination :: Keyset :: Order . build ( [
Gitlab :: Pagination :: Keyset :: ColumnOrderDefinition . new (
2021-10-27 15:23:28 +05:30
attribute_name : " merge_request_metrics_ #{ metric } " ,
2022-06-21 17:19:12 +05:30
column_expression : column_expression ,
order_expression : column_expression_with_direction . nulls_last ,
reversed_order_expression : column_expression_with_direction . reverse . nulls_first ,
2021-04-17 20:07:23 +05:30
order_direction : direction ,
nullable : :nulls_last ,
distinct : false ,
add_to_projections : true
) ,
Gitlab :: Pagination :: Keyset :: ColumnOrderDefinition . new (
attribute_name : 'merge_request_metrics_id' ,
order_expression : MergeRequest :: Metrics . arel_table [ :id ] . desc ,
add_to_projections : true
)
] )
order . apply_cursor_conditions ( join_metrics ) . order ( order )
2020-11-24 15:15:51 +05:30
end
2021-10-27 15:23:28 +05:30
scope :order_merged_at_asc , - > { order_by_metric ( :merged_at , 'ASC' ) }
scope :order_merged_at_desc , - > { order_by_metric ( :merged_at , 'DESC' ) }
scope :order_closed_at_asc , - > { order_by_metric ( :latest_closed_at , 'ASC' ) }
scope :order_closed_at_desc , - > { order_by_metric ( :latest_closed_at , 'DESC' ) }
2019-12-04 20:38:33 +05:30
scope :preload_source_project , - > { preload ( :source_project ) }
2020-10-24 23:57:45 +05:30
scope :preload_target_project , - > { preload ( :target_project ) }
scope :preload_routables , - > do
preload ( target_project : [ :route , { namespace : :route } ] ,
source_project : [ :route , { namespace : :route } ] )
end
scope :preload_author , - > { preload ( :author ) }
scope :preload_approved_by_users , - > { preload ( :approved_by_users ) }
scope :preload_metrics , - > ( relation ) { preload ( metrics : relation ) }
2021-04-17 20:07:23 +05:30
scope :preload_project_and_latest_diff , - > { preload ( :source_project , :latest_merge_request_diff ) }
2021-09-30 23:02:18 +05:30
scope :preload_latest_diff_commit , - > { preload ( latest_merge_request_diff : { merge_request_diff_commits : [ :commit_author , :committer ] } ) }
2021-04-29 21:17:54 +05:30
scope :preload_milestoneish_associations , - > { preload_routables . preload ( :assignees , :labels ) }
scope :with_web_entity_associations , - > { preload ( :author , target_project : [ :project_feature , group : [ :route , :parent ] , namespace : :route ] ) }
2017-08-17 22:00:37 +05:30
2019-12-26 22:10:19 +05:30
scope :with_auto_merge_enabled , - > do
with_state ( :opened ) . where ( auto_merge_enabled : true )
2019-12-21 20:55:43 +05:30
end
2020-04-08 14:13:33 +05:30
scope :including_metrics , - > do
includes ( :metrics )
end
2021-01-29 00:20:46 +05:30
scope :with_jira_issue_keys , - > { where ( 'title ~ :regex OR merge_requests.description ~ :regex' , regex : Gitlab :: Regex . jira_issue_key_regex . source ) }
2021-02-22 17:27:13 +05:30
scope :review_requested , - > do
where ( reviewers_subquery . exists )
end
scope :no_review_requested , - > do
where ( reviewers_subquery . exists . not )
end
scope :review_requested_to , - > ( user ) do
where (
reviewers_subquery
2021-06-08 01:23:25 +05:30
. where ( Arel :: Table . new ( " #{ to_ability_name } _reviewers " ) [ :user_id ] . eq ( user . id ) )
2021-02-22 17:27:13 +05:30
. exists
)
end
scope :no_review_requested_to , - > ( user ) do
where (
reviewers_subquery
2021-06-08 01:23:25 +05:30
. where ( Arel :: Table . new ( " #{ to_ability_name } _reviewers " ) [ :user_id ] . eq ( user . id ) )
2021-02-22 17:27:13 +05:30
. exists
. not
)
end
2022-04-04 11:22:00 +05:30
scope :attention , - > ( user ) do
# rubocop: disable Gitlab/Union
union = Gitlab :: SQL :: Union . new ( [
MergeRequestReviewer . select ( :merge_request_id ) . where ( user_id : user . id , state : MergeRequestReviewer . states [ :attention_requested ] ) ,
MergeRequestAssignee . select ( :merge_request_id ) . where ( user_id : user . id , state : MergeRequestAssignee . states [ :attention_requested ] )
] )
# rubocop: enable Gitlab/Union
with ( Gitlab :: SQL :: CTE . new ( :reviewers_and_assignees , union ) . to_arel ) . where ( 'merge_requests.id in (select merge_request_id from reviewers_and_assignees)' )
end
2021-02-22 17:27:13 +05:30
def self . total_time_to_merge
join_metrics
. merge ( MergeRequest :: Metrics . with_valid_time_to_merge )
. pluck ( MergeRequest :: Metrics . time_to_merge_expression )
. first
end
2020-01-01 13:55:28 +05:30
after_save :keep_around_commit , unless : :importing?
2016-08-24 12:49:21 +05:30
2019-03-13 22:55:13 +05:30
alias_attribute :project , :target_project
alias_attribute :project_id , :target_project_id
2019-12-26 22:10:19 +05:30
# Currently, `merge_when_pipeline_succeeds` column is used as a flag
# to check if _any_ auto merge strategy is activated on the merge request.
# Today, we have multiple strategies and MWPS is one of them.
# we'd eventually rename the column for avoiding confusions, but in the mean time
# please use `auto_merge_enabled` alias instead of `merge_when_pipeline_succeeds`.
2019-09-04 21:01:54 +05:30
alias_attribute :auto_merge_enabled , :merge_when_pipeline_succeeds
2019-10-12 21:52:04 +05:30
alias_method :issuing_parent , :target_project
2019-03-13 22:55:13 +05:30
2021-04-17 20:07:23 +05:30
delegate :builds_with_coverage , to : :head_pipeline , prefix : true , allow_nil : true
2020-01-01 13:55:28 +05:30
2019-12-26 22:10:19 +05:30
RebaseLockTimeout = Class . new ( StandardError )
2015-09-11 14:41:01 +05:30
def self . reference_prefix
'!'
end
2019-07-07 11:18:12 +05:30
# 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 )
2020-01-01 13:55:28 +05:30
. reorder ( arel_table [ :updated_at ] . maximum . desc )
2019-07-07 11:18:12 +05:30
. limit ( limit )
. pluck ( :target_branch )
end
2020-11-24 15:15:51 +05:30
def self . sort_by_attribute ( method , excluded_labels : [ ] )
case method . to_s
2021-04-17 20:07:23 +05:30
when 'merged_at' , 'merged_at_asc' then order_merged_at_asc
2021-10-27 15:23:28 +05:30
when 'closed_at' , 'closed_at_asc' then order_closed_at_asc
2021-04-17 20:07:23 +05:30
when 'merged_at_desc' then order_merged_at_desc
2021-10-27 15:23:28 +05:30
when 'closed_at_desc' then order_closed_at_desc
2020-11-24 15:15:51 +05:30
else
super
end
end
2021-02-22 17:27:13 +05:30
def self . reviewers_subquery
MergeRequestReviewer . arel_table
. project ( 'true' )
. where ( Arel :: Nodes :: SqlLiteral . new ( " #{ to_ability_name } _id = #{ to_ability_name } s.id " ) )
end
2018-03-17 18:26:18 +05:30
def rebase_in_progress?
2019-10-12 21:52:04 +05:30
rebase_jid . present? && Gitlab :: SidekiqStatus . running? ( rebase_jid )
2018-03-17 18:26:18 +05:30
end
2022-04-04 11:22:00 +05:30
def permits_force_push?
return true unless ProtectedBranch . protected? ( source_project , source_branch )
ProtectedBranch . allow_force_push? ( source_project , source_branch )
end
2018-03-17 18:26:18 +05:30
# 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.
2019-12-04 20:38:33 +05:30
# For more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/40004
2018-03-17 18:26:18 +05:30
def actual_head_pipeline
2019-07-07 11:18:12 +05:30
head_pipeline & . matches_sha_or_source_sha? ( diff_head_sha ) ? head_pipeline : nil
2018-03-17 18:26:18 +05:30
end
2018-12-13 13:39:08 +05:30
def merge_pipeline
return unless merged?
2020-11-24 15:15:51 +05:30
# When the merge_method is :merge there will be a merge_commit_sha, however
# when it is fast-forward there is no merge commit, so we must fall back to
# either the squash commit (if the MR was squashed) or the diff head commit.
sha = merge_commit_sha || squash_commit_sha || diff_head_sha
target_project . latest_pipeline ( target_branch , sha )
2018-12-13 13:39:08 +05:30
end
2021-04-17 20:07:23 +05:30
def head_pipeline_active?
! ! head_pipeline & . active?
end
def actual_head_pipeline_active?
! ! actual_head_pipeline & . active?
end
def actual_head_pipeline_success?
! ! actual_head_pipeline & . success?
end
2015-09-11 14:41:01 +05:30
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
def self . reference_pattern
2016-06-02 11:05:42 +05:30
@reference_pattern || = %r{
2015-09-11 14:41:01 +05:30
( #{Project.reference_pattern})?
2022-01-26 12:08:38 +05:30
#{Regexp.escape(reference_prefix)}#{Gitlab::Regex.merge_request}
2015-09-11 14:41:01 +05:30
} x
end
2015-12-23 02:04:40 +05:30
def self . link_reference_pattern
2022-01-26 12:08:38 +05:30
@link_reference_pattern || = super ( " merge_requests " , Gitlab :: Regex . merge_request )
2016-06-02 11:05:42 +05:30
end
2016-06-22 15:30:34 +05:30
def self . reference_valid? ( reference )
reference . to_i > 0 && reference . to_i < = Gitlab :: Database :: MAX_INT_VALUE
end
2016-11-03 12:29:30 +05:30
def self . project_foreign_key
'target_project_id'
end
2016-06-02 11:05:42 +05:30
# 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 )
2017-08-17 22:00:37 +05:30
# unscoping unnecessary conditions that'll be applied
# when executing `where("merge_requests.id IN (#{union.to_sql})")`
2018-12-05 23:21:45 +05:30
source = unscoped . where ( source_project_id : relation )
target = unscoped . where ( target_project_id : relation )
2016-06-02 11:05:42 +05:30
2018-12-05 23:21:45 +05:30
from_union ( [ source , target ] )
2018-03-17 18:26:18 +05:30
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.
2021-03-11 19:13:27 +05:30
#
# Only set `regular` merge request diffs as latest so `merge_head` diff
# won't be considered as `MergeRequest#merge_request_diff`.
2018-03-17 18:26:18 +05:30
def self . set_latest_merge_request_diff_ids!
2021-03-11 19:13:27 +05:30
update = "
2018-03-17 18:26:18 +05:30
latest_merge_request_diff_id = (
SELECT MAX ( id )
FROM merge_request_diffs
WHERE merge_requests . id = merge_request_diffs . merge_request_id
2021-03-11 19:13:27 +05:30
AND merge_request_diffs . diff_type = #{MergeRequestDiff.diff_types[:regular]}
) " .squish
2018-03-17 18:26:18 +05:30
self . each_batch do | batch |
batch . update_all ( update )
end
2015-12-23 02:04:40 +05:30
end
2022-04-04 11:22:00 +05:30
DRAFT_REGEX = / \ A* #{ Gitlab :: Regex . merge_request_draft } + \ s* /i . freeze
2016-11-03 12:29:30 +05:30
2022-04-04 11:22:00 +05:30
def self . draft? ( title )
2020-07-28 23:09:34 +05:30
! ! ( title =~ DRAFT_REGEX )
2016-11-03 12:29:30 +05:30
end
2022-04-04 11:22:00 +05:30
def self . draftless_title ( title )
2020-07-28 23:09:34 +05:30
title . sub ( DRAFT_REGEX , " " )
2016-11-03 12:29:30 +05:30
end
2022-04-04 11:22:00 +05:30
def self . draft_title ( title )
draft? ( title ) ? title : " Draft: #{ title } "
end
class << self
alias_method :work_in_progress? , :draft?
alias_method :wipless_title , :draftless_title
alias_method :wip_title , :draft_title
2016-11-03 12:29:30 +05:30
end
2021-03-08 18:12:59 +05:30
def self . participant_includes
[ :reviewers , :award_emoji ] + super
end
2019-07-07 11:18:12 +05:30
def committers
@committers || = commits . committers
2019-03-02 22:35:43 +05:30
end
2020-07-28 23:09:34 +05:30
# Verifies if title has changed not taking into account Draft prefix
2018-03-17 18:26:18 +05:30
# for merge requests.
2022-04-04 11:22:00 +05:30
def draftless_title_changed ( old_title )
self . class . draftless_title ( old_title ) != self . draftless_title
2018-03-17 18:26:18 +05:30
end
2022-04-04 11:22:00 +05:30
alias_method :wipless_title_changed , :draftless_title_changed
2018-03-17 18:26:18 +05:30
def hook_attrs
Gitlab :: HookData :: MergeRequestBuilder . new ( self ) . build
end
2017-08-17 22:00:37 +05:30
# `from` argument can be a Namespace or Project.
def to_reference ( from = nil , full : false )
reference = " #{ self . class . reference_prefix } #{ iid } "
2020-03-13 15:44:24 +05:30
" #{ project . to_reference_base ( from , full : full ) } #{ reference } "
end
2020-04-22 19:07:51 +05:30
def context_commits ( limit : nil )
2020-10-24 23:57:45 +05:30
@context_commits || = merge_request_context_commits . order_by_committed_date_desc . limit ( limit ) . map ( & :to_commit )
2020-04-22 19:07:51 +05:30
end
def recent_context_commits
context_commits ( limit : MergeRequestDiff :: COMMITS_SAFE_SIZE )
end
def context_commits_count
context_commits . count
2015-09-11 14:41:01 +05:30
end
2014-09-02 18:07:02 +05:30
2021-11-11 11:23:49 +05:30
def commits ( limit : nil , load_from_gitaly : false )
return merge_request_diff . commits ( limit : limit , load_from_gitaly : load_from_gitaly ) if merge_request_diff . persisted?
2019-03-02 22:35:43 +05:30
commits_arr = if compare_commits
2019-12-26 22:10:19 +05:30
reversed_commits = compare_commits . reverse
limit ? reversed_commits . take ( limit ) : reversed_commits
2019-03-02 22:35:43 +05:30
else
[ ]
end
CommitCollection . new ( source_project , commits_arr , source_branch )
2017-09-10 17:25:29 +05:30
end
2021-11-11 11:23:49 +05:30
def recent_commits ( load_from_gitaly : false )
commits ( limit : MergeRequestDiff :: COMMITS_SAFE_SIZE , load_from_gitaly : load_from_gitaly )
2019-12-26 22:10:19 +05:30
end
2017-09-10 17:25:29 +05:30
def commits_count
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2017-09-10 17:25:29 +05:30
merge_request_diff . commits_count
elsif compare_commits
compare_commits . size
else
0
end
end
2019-12-26 22:10:19 +05:30
def commit_shas ( limit : nil )
2020-04-08 14:13:33 +05:30
return merge_request_diff . commit_shas ( limit : limit ) if merge_request_diff . persisted?
2019-12-26 22:10:19 +05:30
shas =
if compare_commits
compare_commits . to_a . reverse . map ( & :sha )
else
Array ( diff_head_sha )
end
limit ? shas . take ( limit ) : shas
2017-09-10 17:25:29 +05:30
end
2019-02-15 15:39:39 +05:30
def supports_suggestion?
true
end
2018-03-17 18:26:18 +05:30
# 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 )
2021-12-11 22:18:48 +05:30
jid = MergeWorker . with_status . perform_async ( id , user_id , params . to_h )
2018-03-17 18:26:18 +05:30
update_column ( :merge_jid , jid )
2019-10-12 21:52:04 +05:30
# merge_ongoing? depends on merge_jid
# expire etag cache since the attribute is changed without triggering callbacks
expire_etag_cache
2018-03-17 18:26:18 +05:30
end
2019-09-30 21:07:59 +05:30
# Set off a rebase asynchronously, atomically updating the `rebase_jid` of
# the MR so that the status of the operation can be tracked.
2020-03-13 15:44:24 +05:30
def rebase_async ( user_id , skip_ci : false )
2019-12-26 22:10:19 +05:30
with_rebase_lock do
2019-09-30 21:07:59 +05:30
raise ActiveRecord :: StaleObjectError if ! open ? || rebase_in_progress?
# Although there is a race between setting rebase_jid here and clearing it
# in the RebaseWorker, it can't do any harm since we check both that the
# attribute is set *and* that the sidekiq job is still running. So a JID
# for a completed RebaseWorker is equivalent to a nil JID.
jid = Sidekiq :: Worker . skipping_transaction_check do
2021-12-11 22:18:48 +05:30
RebaseWorker . with_status . perform_async ( id , user_id , skip_ci )
2019-09-30 21:07:59 +05:30
end
update_column ( :rebase_jid , jid )
end
2019-10-12 21:52:04 +05:30
# rebase_in_progress? depends on rebase_jid
# expire etag cache since the attribute is changed without triggering callbacks
expire_etag_cache
2019-09-30 21:07:59 +05:30
end
2018-11-08 19:23:39 +05:30
def merge_participants
participants = [ author ]
2019-09-04 21:01:54 +05:30
if auto_merge_enabled? && ! participants . include? ( merge_user )
2018-11-08 19:23:39 +05:30
participants << merge_user
end
2020-07-02 01:45:43 +05:30
participants . select { | participant | Ability . allowed? ( participant , :read_merge_request , self ) }
2018-11-08 19:23:39 +05:30
end
2015-10-24 18:46:33 +05:30
def first_commit
2020-04-08 14:13:33 +05:30
compare_commits . present? ? compare_commits . first : merge_request_diff . first_commit
2015-11-26 14:37:03 +05:30
end
2015-10-24 18:46:33 +05:30
2016-09-13 17:45:13 +05:30
def raw_diffs ( * args )
2020-04-08 14:13:33 +05:30
compare . present? ? compare . raw_diffs ( * args ) : merge_request_diff . raw_diffs ( * args )
2016-09-13 17:45:13 +05:30
end
2017-08-17 22:00:37 +05:30
def diffs ( diff_options = { } )
2016-09-29 09:46:39 +05:30
if compare
2017-09-10 17:25:29 +05:30
# When saving MR diffs, `expanded` is implicitly added (because we need
2017-08-17 22:00:37 +05:30
# to save the entire contents to the DB), so add that here for
# consistency.
2017-09-10 17:25:29 +05:30
compare . diffs ( diff_options . merge ( expanded : true ) )
2016-09-13 17:45:13 +05:30
else
2016-09-29 09:46:39 +05:30
merge_request_diff . diffs ( diff_options )
2016-09-13 17:45:13 +05:30
end
2016-08-24 12:49:21 +05:30
end
2018-11-08 19:23:39 +05:30
def non_latest_diffs
merge_request_diffs . where . not ( id : merge_request_diff . id )
end
2020-06-23 00:09:42 +05:30
def note_positions_for_paths ( paths , user = nil )
2019-12-21 20:55:43 +05:30
positions = notes . new_diff_notes . joins ( :note_diff_file )
. where ( 'note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)' , paths , paths )
. positions
2020-06-23 00:09:42 +05:30
collection = Gitlab :: Diff :: PositionCollection . new ( positions , diff_head_sha )
return collection unless user
positions = draft_notes
. authored_by ( user )
. positions
. select { | pos | paths . include? ( pos . file_path ) }
collection . concat ( positions )
2019-12-21 20:55:43 +05:30
end
2019-02-15 15:39:39 +05:30
def preloads_discussion_diff_highlighting?
true
end
def discussions_diffs
strong_memoize ( :discussions_diffs ) do
2019-12-04 20:38:33 +05:30
note_diff_files = NoteDiffFile
. joins ( :diff_note )
. merge ( notes . or ( commit_notes ) )
. includes ( diff_note : :project )
2019-02-15 15:39:39 +05:30
Gitlab :: DiscussionsDiff :: FileCollection . new ( note_diff_files . to_a )
end
end
2020-04-22 19:07:51 +05:30
def diff_stats
return unless diff_refs
strong_memoize ( :diff_stats ) do
project . repository . diff_stats ( diff_refs . base_sha , diff_refs . head_sha )
end
end
2016-06-02 11:05:42 +05:30
def diff_size
2017-08-17 22:00:37 +05:30
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
2022-01-26 12:08:38 +05:30
merge_request_diff & . real_size || diff_stats & . real_size || diffs . real_size
2016-06-02 11:05:42 +05:30
end
2020-04-22 19:07:51 +05:30
def modified_paths ( past_merge_request_diff : nil , fallback_on_overflow : false )
if past_merge_request_diff
past_merge_request_diff . modified_paths ( fallback_on_overflow : fallback_on_overflow )
elsif compare
diff_stats & . paths || compare . modified_paths
else
merge_request_diff . modified_paths ( fallback_on_overflow : fallback_on_overflow )
end
2018-12-13 13:39:08 +05:30
end
2020-04-08 14:13:33 +05:30
def new_paths
diffs . diff_files . map ( & :new_path )
end
2016-01-29 22:53:50 +05:30
def diff_base_commit
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2016-01-29 22:53:50 +05:30
merge_request_diff . base_commit
2016-09-29 09:46:39 +05:30
else
branch_merge_base_commit
2016-01-29 22:53:50 +05:30
end
end
2016-08-24 12:49:21 +05:30
def diff_start_commit
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2016-08-24 12:49:21 +05:30
merge_request_diff . start_commit
else
target_branch_head
end
end
def diff_head_commit
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2016-08-24 12:49:21 +05:30
merge_request_diff . head_commit
else
source_branch_head
end
end
def diff_start_sha
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2018-03-27 19:54:05 +05:30
merge_request_diff . start_commit_sha
else
target_branch_head . try ( :sha )
end
2016-08-24 12:49:21 +05:30
end
def diff_base_sha
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2018-03-27 19:54:05 +05:30
merge_request_diff . base_commit_sha
else
branch_merge_base_commit . try ( :sha )
end
2016-08-24 12:49:21 +05:30
end
def diff_head_sha
2020-04-08 14:13:33 +05:30
if merge_request_diff . persisted?
2018-03-27 19:54:05 +05:30
merge_request_diff . head_commit_sha
else
source_branch_head . try ( :sha )
end
2016-08-24 12:49:21 +05:30
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
2018-03-17 18:26:18 +05:30
def source_branch_ref
return @source_branch_sha if @source_branch_sha
return unless source_branch
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
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
2016-08-24 12:49:21 +05:30
end
def target_branch_head
2018-03-17 18:26:18 +05:30
strong_memoize ( :target_branch_head ) do
target_project . repository . commit ( target_branch_ref )
end
2016-09-29 09:46:39 +05:30
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
2016-08-24 12:49:21 +05:30
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
2019-09-30 21:07:59 +05:30
if importing? || persisted?
merge_request_diff . diff_refs
else
repository_diff_refs
end
2019-02-15 15:39:39 +05:30
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
)
2016-09-13 17:45:13 +05:30
end
2016-09-29 09:46:39 +05:30
def branch_merge_base_sha
branch_merge_base_commit . try ( :sha )
end
2014-09-02 18:07:02 +05:30
def validate_branches
2019-09-04 21:01:54 +05:30
return unless target_project && source_project
2014-09-02 18:07:02 +05:30
if target_project == source_project && target_branch == source_branch
2019-02-15 15:39:39 +05:30
errors . add :branch_conflict , " You can't use same project/branch for source and target "
return
2014-09-02 18:07:02 +05:30
end
2019-06-05 12:25:43 +05:30
[ :source_branch , :target_branch ] . each { | attr | validate_branch_name ( attr ) }
2017-09-10 17:25:29 +05:30
if opened?
2019-02-15 15:39:39 +05:30
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 } "
)
2014-09-02 18:07:02 +05:30
end
end
end
2019-06-05 12:25:43 +05:30
def validate_branch_name ( attr )
2020-03-13 15:44:24 +05:30
return unless will_save_change_to_attribute? ( attr )
2019-06-05 12:25:43 +05:30
branch = read_attribute ( attr )
return unless branch
errors . add ( attr ) unless Gitlab :: GitRefValidator . validate_merge_request_branch ( branch )
end
2017-08-17 22:00:37 +05:30
def validate_target_project
return true if target_project . merge_requests_enabled?
errors . add :base , 'Target project has disabled merge requests'
end
2014-09-02 18:07:02 +05:30
def validate_fork
return true unless target_project && source_project
2016-09-29 09:46:39 +05:30
return true if target_project == source_project
2016-11-03 12:29:30 +05:30
return true unless source_project_missing?
2014-09-02 18:07:02 +05:30
2016-09-29 09:46:39 +05:30
errors . add :validate_fork ,
'Source project is not a fork of the target project'
2014-09-02 18:07:02 +05:30
end
2017-09-10 17:25:29 +05:30
def merge_ongoing?
2018-03-17 18:26:18 +05:30
# 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?
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
! ! merge_jid && ! merged? && Gitlab :: SidekiqStatus . running? ( merge_jid )
2017-09-10 17:25:29 +05:30
end
2021-02-22 17:27:13 +05:30
def closed_or_merged_without_fork?
( closed? || merged? ) && source_project_missing?
2016-09-29 09:46:39 +05:30
end
2016-11-03 12:29:30 +05:30
def source_project_missing?
2016-09-29 09:46:39 +05:30
return false unless for_fork?
return true unless source_project
2018-03-17 18:26:18 +05:30
! source_project . in_fork_network_of? ( target_project )
2016-09-29 09:46:39 +05:30
end
def reopenable?
2016-11-03 12:29:30 +05:30
closed? && ! source_project_missing? && source_branch_exists?
2016-09-29 09:46:39 +05:30
end
2021-03-11 19:13:27 +05:30
def can_be_closed?
opened?
end
2016-09-29 09:46:39 +05:30
def ensure_merge_request_diff
2020-04-08 14:13:33 +05:30
merge_request_diff . persisted? || create_merge_request_diff
2016-09-29 09:46:39 +05:30
end
2022-05-07 20:08:51 +05:30
def eager_fetch_ref!
return unless valid?
# has_internal_id normally attempts to allocate the iid in the
# before_create hook, but we need the iid to be available before
# that to fetch the ref into the target project.
track_target_project_iid!
ensure_target_project_iid!
2018-03-17 18:26:18 +05:30
fetch_ref!
2022-05-07 20:08:51 +05:30
# Prevent the after_create hook from fetching the source branch again.
@skip_fetch_ref = true
end
def create_merge_request_diff
# Callers such as MergeRequests::BuildService may not call eager_fetch_ref!. Just
# in case they haven't, we fetch the ref.
fetch_ref! unless skip_fetch_ref
2018-03-17 18:26:18 +05:30
2021-03-11 19:13:27 +05:30
# n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377
2018-03-17 18:26:18 +05:30
Gitlab :: GitalyClient . allow_n_plus_1_calls do
2019-09-04 21:01:54 +05:30
merge_request_diffs . create!
2018-03-17 18:26:18 +05:30
reload_merge_request_diff
end
2016-09-29 09:46:39 +05:30
end
2018-05-09 12:01:36 +05:30
def viewable_diffs
@viewable_diffs || = merge_request_diffs . viewable . to_a
end
2017-08-17 22:00:37 +05:30
def merge_request_diff_for ( diff_refs_or_sha )
2018-05-09 12:01:36 +05:30
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
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
viewable_diffs . find do | diff |
diff . attributes . slice ( * matcher . keys ) == matcher
end
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
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
2018-03-17 18:26:18 +05:30
def clear_memoized_shas
@target_branch_sha = @source_branch_sha = nil
clear_memoization ( :source_branch_head )
clear_memoization ( :target_branch_head )
end
2016-09-29 09:46:39 +05:30
def reload_diff_if_branch_changed
2019-07-31 22:56:46 +05:30
if ( saved_change_to_source_branch? || saved_change_to_target_branch? ) &&
2018-03-17 18:26:18 +05:30
( source_branch_head && target_branch_head )
2016-08-24 12:49:21 +05:30
reload_diff
2014-09-02 18:07:02 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2017-09-10 17:25:29 +05:30
def reload_diff ( current_user = nil )
2016-09-29 09:46:39 +05:30
return unless open ?
2016-08-24 12:49:21 +05:30
2018-11-08 19:23:39 +05:30
MergeRequests :: ReloadDiffsService . new ( self , current_user ) . execute
2014-09-02 18:07:02 +05:30
end
2016-01-14 18:37:52 +05:30
2020-03-13 15:44:24 +05:30
def check_mergeability ( async : false )
return unless recheck_merge_status?
2019-12-26 22:10:19 +05:30
2020-03-13 15:44:24 +05:30
check_service = MergeRequests :: MergeabilityCheckService . new ( self )
2020-06-23 00:09:42 +05:30
if async
2020-03-13 15:44:24 +05:30
check_service . async_execute
else
check_service . execute ( retry_lease : false )
end
2019-09-30 21:07:59 +05:30
end
# rubocop: enable CodeReuse/ServiceClass
2015-09-25 12:07:36 +05:30
2020-03-13 15:44:24 +05:30
def diffable_merge_ref?
2021-03-11 19:13:27 +05:30
open ? && merge_head_diff . present? && ( Feature . enabled? ( :display_merge_conflicts_in_diff , project ) || can_be_merged? )
2020-03-13 15:44:24 +05:30
end
2019-09-30 21:07:59 +05:30
# Returns boolean indicating the merge_status should be rechecked in order to
# switch to either can_be_merged or cannot_be_merged.
def recheck_merge_status?
self . class . state_machines [ :merge_status ] . check_state? ( merge_status )
2014-09-02 18:07:02 +05:30
end
def merge_event
2020-06-23 00:09:42 +05:30
@merge_event || = target_project . events . where ( target_id : self . id , target_type : " MergeRequest " , action : :merged ) . last
2014-09-02 18:07:02 +05:30
end
def closed_event
2020-06-23 00:09:42 +05:30
@closed_event || = target_project . events . where ( target_id : self . id , target_type : " MergeRequest " , action : :closed ) . last
2014-09-02 18:07:02 +05:30
end
2022-04-04 11:22:00 +05:30
def draft?
self . class . draft? ( title )
2016-06-02 11:05:42 +05:30
end
2022-04-04 11:22:00 +05:30
alias_method :work_in_progress? , :draft?
2016-06-02 11:05:42 +05:30
2022-04-04 11:22:00 +05:30
def draftless_title
self . class . draftless_title ( self . title )
2016-11-03 12:29:30 +05:30
end
2022-04-04 11:22:00 +05:30
alias_method :wipless_title , :draftless_title
2016-11-03 12:29:30 +05:30
2022-04-04 11:22:00 +05:30
def draft_title
self . class . draft_title ( self . title )
2015-09-11 14:41:01 +05:30
end
2022-04-04 11:22:00 +05:30
alias_method :wip_title , :draft_title
2015-09-11 14:41:01 +05:30
2020-11-24 15:15:51 +05:30
def mergeable? ( skip_ci_check : false , skip_discussions_check : false )
return false unless mergeable_state? ( skip_ci_check : skip_ci_check ,
skip_discussions_check : skip_discussions_check )
2016-01-14 18:37:52 +05:30
2019-09-30 21:07:59 +05:30
check_mergeability
2016-01-14 18:37:52 +05:30
2018-03-17 18:26:18 +05:30
can_be_merged? && ! should_be_rebased?
2015-09-11 14:41:01 +05:30
end
2021-11-18 22:05:49 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def mergeable_state? ( skip_ci_check : false , skip_discussions_check : false )
2021-11-18 22:05:49 +05:30
if Feature . enabled? ( :improved_mergeability_checks , self . project , default_enabled : :yaml )
2022-05-07 20:08:51 +05:30
additional_checks = MergeRequests :: Mergeability :: RunChecksService . new (
merge_request : self ,
params : {
skip_ci_check : skip_ci_check ,
skip_discussions_check : skip_discussions_check
}
)
2021-11-18 22:05:49 +05:30
additional_checks . execute . all? ( & :success? )
else
2022-05-07 20:08:51 +05:30
return false unless open ?
return false if draft?
return false if broken?
return false unless skip_discussions_check || mergeable_discussions_state?
2021-11-18 22:05:49 +05:30
return false unless skip_ci_check || mergeable_ci_state?
true
end
2015-09-11 14:41:01 +05:30
end
2021-11-18 22:05:49 +05:30
# rubocop: enable CodeReuse/ServiceClass
2015-09-11 14:41:01 +05:30
2018-03-17 18:26:18 +05:30
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
2019-09-04 21:01:54 +05:30
def can_cancel_auto_merge? ( current_user )
2015-12-23 02:04:40 +05:30
can_be_merged_by? ( current_user ) || self . author == current_user
end
def can_remove_source_branch? ( current_user )
2020-07-28 23:09:34 +05:30
source_project &&
! ProtectedBranch . protected? ( source_project , source_branch ) &&
2015-12-23 02:04:40 +05:30
! source_project . root_ref? ( source_branch ) &&
2016-09-29 09:46:39 +05:30
Ability . allowed? ( current_user , :push_code , source_project ) &&
2018-03-27 19:54:05 +05:30
diff_head_sha == source_branch_head . try ( :sha )
2015-12-23 02:04:40 +05:30
end
2016-06-02 11:05:42 +05:30
def should_remove_source_branch?
2016-11-24 13:41:30 +05:30
Gitlab :: Utils . to_boolean ( merge_params [ 'should_remove_source_branch' ] )
2016-06-02 11:05:42 +05:30
end
def force_remove_source_branch?
2016-11-24 13:41:30 +05:30
Gitlab :: Utils . to_boolean ( merge_params [ 'force_remove_source_branch' ] )
2016-06-02 11:05:42 +05:30
end
2019-09-04 21:01:54 +05:30
def auto_merge_strategy
return unless auto_merge_enabled?
merge_params [ 'auto_merge_strategy' ] || AutoMergeService :: STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
end
def auto_merge_strategy = ( strategy )
merge_params [ 'auto_merge_strategy' ] = strategy
end
2016-06-02 11:05:42 +05:30
def remove_source_branch?
should_remove_source_branch? || force_remove_source_branch?
end
2018-11-08 19:23:39 +05:30
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
2017-01-15 13:20:01 +05:30
def related_notes
2018-03-17 18:26:18 +05:30
# 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).
2018-12-05 23:21:45 +05:30
Note
. from_union ( [ notes , commit_notes ] , remove_duplicates : false )
2018-03-17 18:26:18 +05:30
. includes ( :noteable )
2014-09-02 18:07:02 +05:30
end
2017-08-17 22:00:37 +05:30
alias_method :discussion_notes , :related_notes
2016-09-13 17:45:13 +05:30
2019-07-07 11:18:12 +05:30
def commit_notes
# Fetch comments only from last 100 commits
2019-12-26 22:10:19 +05:30
commit_ids = commit_shas ( limit : 100 )
2019-07-07 11:18:12 +05:30
Note
. user
. where ( project_id : [ source_project_id , target_project_id ] )
. for_commit_id ( commit_ids )
end
2017-08-17 22:00:37 +05:30
def mergeable_discussions_state?
return true unless project . only_allow_merge_if_all_discussions_are_resolved?
2016-09-13 17:45:13 +05:30
2019-12-26 22:10:19 +05:30
unresolved_notes . none? ( & :to_be_resolved? )
2016-09-13 17:45:13 +05:30
end
2014-09-02 18:07:02 +05:30
def for_fork?
target_project != source_project
end
2020-07-28 23:09:34 +05:30
def for_same_project?
target_project == source_project
end
2016-09-29 09:46:39 +05:30
# 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.
2016-11-03 12:29:30 +05:30
# This optimization does not apply to issues from external sources.
2018-11-18 11:00:15 +05:30
def cache_merge_request_closes_issues! ( current_user = self . author )
2017-09-10 17:25:29 +05:30
return unless project . issues_enabled?
2018-11-18 11:00:15 +05:30
return if closed? || merged?
2016-11-03 12:29:30 +05:30
2016-09-29 09:46:39 +05:30
transaction do
self . merge_requests_closing_issues . delete_all
2016-11-03 12:29:30 +05:30
2016-09-29 09:46:39 +05:30
closes_issues ( current_user ) . each do | issue |
2017-09-10 17:25:29 +05:30
next if issue . is_a? ( ExternalIssue )
2016-09-29 09:46:39 +05:30
self . merge_requests_closing_issues . create! ( issue : issue )
end
end
end
2018-11-18 11:00:15 +05:30
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
2014-09-02 18:07:02 +05:30
# Return the set of issues that will be closed if this merge request is accepted.
2015-04-26 12:48:37 +05:30
def closes_issues ( current_user = self . author )
2014-09-02 18:07:02 +05:30
if target_branch == project . default_branch
2017-08-17 22:00:37 +05:30
messages = [ title , description ]
2020-04-08 14:13:33 +05:30
messages . concat ( commits . map ( & :safe_message ) ) if merge_request_diff . persisted?
2016-04-02 18:10:28 +05:30
2017-09-10 17:25:29 +05:30
Gitlab :: ClosingIssueExtractor . new ( project , current_user )
. closed_by_message ( messages . join ( " \n " ) )
2014-09-02 18:07:02 +05:30
else
[ ]
end
end
2017-08-17 22:00:37 +05:30
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 } " )
2018-11-18 11:00:15 +05:30
ext . issues - visible_closing_issues_for ( current_user )
2017-08-17 22:00:37 +05:30
end
2014-09-02 18:07:02 +05:30
def target_project_path
if target_project
2017-09-10 17:25:29 +05:30
target_project . full_path
2014-09-02 18:07:02 +05:30
else
" (removed) "
end
end
def source_project_path
if source_project
2017-09-10 17:25:29 +05:30
source_project . full_path
2014-09-02 18:07:02 +05:30
else
" (removed) "
end
end
def source_project_namespace
if source_project && source_project . namespace
2017-08-17 22:00:37 +05:30
source_project . namespace . full_path
2014-09-02 18:07:02 +05:30
else
" (removed) "
end
end
def target_project_namespace
if target_project && target_project . namespace
2017-08-17 22:00:37 +05:30
target_project . namespace . full_path
2014-09-02 18:07:02 +05:30
else
" (removed) "
end
end
def source_branch_exists?
2020-11-24 15:15:51 +05:30
return false unless self . source_project
2014-09-02 18:07:02 +05:30
2020-11-24 15:15:51 +05:30
self . source_project . repository . branch_exists? ( self . source_branch )
2014-09-02 18:07:02 +05:30
end
def target_branch_exists?
return false unless self . target_project
2018-03-17 18:26:18 +05:30
self . target_project . repository . branch_exists? ( self . target_branch )
2014-09-02 18:07:02 +05:30
end
2022-03-02 08:16:31 +05:30
def default_merge_commit_message ( include_description : false , user : nil )
2021-12-11 22:18:48 +05:30
if self . target_project . merge_commit_template . present? && ! include_description
2022-03-02 08:16:31 +05:30
return :: Gitlab :: MergeRequests :: CommitMessageGenerator . new ( merge_request : self , current_user : user ) . merge_message
2021-12-11 22:18:48 +05:30
end
2018-11-18 11:00:15 +05:30
closes_issues_references = visible_closing_issues_for . map do | issue |
2017-08-17 22:00:37 +05:30
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?
2018-03-17 18:26:18 +05:30
message << " See merge request #{ to_reference ( full : true ) } "
2016-11-03 12:29:30 +05:30
2017-08-17 22:00:37 +05:30
message . join ( " \n \n " )
2014-09-02 18:07:02 +05:30
end
2022-03-02 08:16:31 +05:30
def default_squash_commit_message ( user : nil )
2022-01-26 12:08:38 +05:30
if self . target_project . squash_commit_template . present?
2022-03-02 08:16:31 +05:30
return :: Gitlab :: MergeRequests :: CommitMessageGenerator . new ( merge_request : self , current_user : user ) . squash_message
2022-01-26 12:08:38 +05:30
end
2021-04-29 21:17:54 +05:30
title
2021-04-17 20:07:23 +05:30
end
# Returns the oldest multi-line commit
def first_multiline_commit
strong_memoize ( :first_multiline_commit ) do
recent_commits . without_merge_commits . reverse_each . find ( & :description? )
2019-03-02 22:35:43 +05:30
end
end
2020-07-28 23:09:34 +05:30
def squash_on_merge?
return true if target_project . squash_always?
return false if target_project . squash_never?
squash?
end
2015-09-11 14:41:01 +05:30
def has_ci?
2018-03-17 18:26:18 +05:30
return false if has_no_commits?
2017-08-17 22:00:37 +05:30
2022-06-21 17:19:12 +05:30
! ! ( head_pipeline_id || all_pipelines . any? || source_project & . ci_integration )
2015-09-11 14:41:01 +05:30
end
def branch_missing?
! source_branch_exists? || ! target_branch_exists?
end
2016-01-14 18:37:52 +05:30
def broken?
2017-08-17 22:00:37 +05:30
has_no_commits? || branch_missing? || cannot_be_merged?
2016-01-14 18:37:52 +05:30
end
2021-04-01 16:36:13 +05:30
def can_be_merged_by? ( user , skip_collaboration_check : false )
access = :: Gitlab :: UserAccess . new ( user , container : project , skip_collaboration_check : skip_collaboration_check )
2018-03-27 19:54:05 +05:30
access . can_update_branch? ( target_branch )
2016-08-24 12:49:21 +05:30
end
def can_be_merged_via_command_line_by? ( user )
2020-10-24 23:57:45 +05:30
access = :: Gitlab :: UserAccess . new ( user , container : project )
2016-08-24 12:49:21 +05:30
access . can_push_to_branch? ( target_branch )
2015-09-11 14:41:01 +05:30
end
2016-06-16 23:09:34 +05:30
def mergeable_ci_state?
2017-08-17 22:00:37 +05:30
return true unless project . only_allow_merge_if_pipeline_succeeds?
2019-09-30 21:07:59 +05:30
return false unless actual_head_pipeline
2020-06-23 00:09:42 +05:30
return true if project . allow_merge_on_skipped_pipeline? && actual_head_pipeline . skipped?
2016-06-16 23:09:34 +05:30
2019-12-26 22:10:19 +05:30
actual_head_pipeline . success?
2016-06-16 23:09:34 +05:30
end
2019-07-07 11:18:12 +05:30
##
# This method is for looking for active environments which created via pipelines for merge requests.
# Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
# we cannot look up environments with source branch name.
2022-06-21 17:19:12 +05:30
def legacy_environments
2020-03-13 15:44:24 +05:30
return Environment . none unless actual_head_pipeline & . merge_request?
2019-07-07 11:18:12 +05:30
2021-12-11 22:18:48 +05:30
build_for_actual_head_pipeline = Ci :: Build . latest . where ( pipeline : actual_head_pipeline )
environments = build_for_actual_head_pipeline . joins ( :metadata )
. where . not ( 'ci_builds_metadata.expanded_environment_name' = > nil )
. distinct ( 'ci_builds_metadata.expanded_environment_name' )
. limit ( 100 )
. pluck ( :expanded_environment_name )
Environment . where ( project : project , name : environments )
2019-07-07 11:18:12 +05:30
end
2022-06-21 17:19:12 +05:30
def environments_in_head_pipeline ( deployment_status : nil )
if :: Feature . enabled? ( :fix_related_environments_for_merge_requests , target_project , default_enabled : :yaml )
actual_head_pipeline & . environments_in_self_and_descendants ( deployment_status : deployment_status ) || Environment . none
else
legacy_environments
end
end
2018-03-17 18:26:18 +05:30
def fetch_ref!
target_project . repository . fetch_source_branch! ( source_project . repository , source_branch , ref_path )
2015-09-25 12:07:36 +05:30
end
2019-09-30 21:07:59 +05:30
# Returns the current merge-ref HEAD commit.
#
def merge_ref_head
2020-11-24 15:15:51 +05:30
return project . repository . commit ( merge_ref_sha ) if merge_ref_sha
2019-09-30 21:07:59 +05:30
project . repository . commit ( merge_ref_path )
end
2015-09-25 12:07:36 +05:30
def ref_path
2018-03-17 18:26:18 +05:30
" refs/ #{ Repository :: REF_MERGE_REQUEST } / #{ iid } /head "
2015-09-25 12:07:36 +05:30
end
2019-07-07 11:18:12 +05:30
def merge_ref_path
" refs/ #{ Repository :: REF_MERGE_REQUEST } / #{ iid } /merge "
end
2019-09-30 21:07:59 +05:30
def train_ref_path
" refs/ #{ Repository :: REF_MERGE_REQUEST } / #{ iid } /train "
end
def cleanup_refs ( only : :all )
target_refs = [ ]
target_refs << ref_path if % i [ all head ] . include? ( only )
target_refs << merge_ref_path if % i [ all merge ] . include? ( only )
target_refs << train_ref_path if % i [ all train ] . include? ( only )
project . repository . delete_refs ( * target_refs )
end
2019-07-07 11:18:12 +05:30
def self . merge_request_ref? ( ref )
ref . start_with? ( " refs/ #{ Repository :: REF_MERGE_REQUEST } / " )
end
2019-12-04 20:38:33 +05:30
def self . merge_train_ref? ( ref )
%r{ \ Arefs/ #{ Repository :: REF_MERGE_REQUEST } / \ d+/train \ z } . match? ( ref )
end
2015-09-25 12:07:36 +05:30
def in_locked_state
2020-03-13 15:44:24 +05:30
lock_mr
yield
ensure
unlock_mr
2015-09-25 12:07:36 +05:30
end
2015-11-26 14:37:03 +05:30
2021-01-03 14:25:43 +05:30
def update_and_mark_in_progress_merge_commit_sha ( commit_id )
self . update ( in_progress_merge_commit_sha : commit_id )
# Since another process checks for matching merge request, we need
# to make it possible to detect whether the query should go to the
# primary.
target_project . mark_primary_write_location
end
2016-06-02 11:05:42 +05:30
def diverged_commits_count
cache = Rails . cache . read ( :" merge_request_ #{ id } _diverged_commits " )
2016-08-24 12:49:21 +05:30
if cache . blank? || cache [ :source_sha ] != source_branch_sha || cache [ :target_sha ] != target_branch_sha
2016-06-02 11:05:42 +05:30
cache = {
2016-08-24 12:49:21 +05:30
source_sha : source_branch_sha ,
target_sha : target_branch_sha ,
2016-06-02 11:05:42 +05:30
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
2016-08-24 12:49:21 +05:30
return 0 unless source_branch_sha && target_branch_sha
2016-06-02 11:05:42 +05:30
2018-03-17 18:26:18 +05:30
target_project . repository
. count_commits_between ( source_branch_sha , target_branch_sha )
2016-06-02 11:05:42 +05:30
end
private :compute_diverged_commits_count
def diverged_from_target_branch?
diverged_commits_count > 0
end
2019-07-07 11:18:12 +05:30
def all_pipelines
strong_memoize ( :all_pipelines ) do
2020-07-28 23:09:34 +05:30
Ci :: PipelinesForMergeRequestFinder . new ( self , nil ) . all
2019-07-07 11:18:12 +05:30
end
2019-02-15 15:39:39 +05:30
end
def update_head_pipeline
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
end
2018-11-18 11:00:15 +05:30
def has_test_reports?
2019-07-07 11:18:12 +05:30
actual_head_pipeline & . has_reports? ( Ci :: JobArtifact . test_reports )
2018-11-18 11:00:15 +05:30
end
2019-02-15 15:39:39 +05:30
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 )
2019-07-07 11:18:12 +05:30
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 )
2020-04-22 19:07:51 +05:30
variables . append ( key : 'CI_MERGE_REQUEST_ASSIGNEES' , value : assignee_username_list ) if assignees . present?
2019-07-07 11:18:12 +05:30
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 )
2019-02-15 15:39:39 +05:30
end
end
2018-11-18 11:00:15 +05:30
def compare_test_reports
unless has_test_reports?
return { status : :error , status_reason : 'This merge request does not have test reports' }
end
2019-02-15 15:39:39 +05:30
compare_reports ( Ci :: CompareTestReportsService )
end
2020-05-24 23:13:21 +05:30
def has_accessibility_reports?
actual_head_pipeline . present? && actual_head_pipeline . has_reports? ( Ci :: JobArtifact . accessibility_reports )
end
2020-04-08 14:13:33 +05:30
def has_coverage_reports?
2020-11-24 15:15:51 +05:30
actual_head_pipeline & . has_coverage_reports?
2020-04-08 14:13:33 +05:30
end
2020-05-24 23:13:21 +05:30
def has_terraform_reports?
actual_head_pipeline & . has_reports? ( Ci :: JobArtifact . terraform_reports )
end
def compare_accessibility_reports
unless has_accessibility_reports?
return { status : :error , status_reason : _ ( 'This merge request does not have accessibility reports' ) }
end
compare_reports ( Ci :: CompareAccessibilityReportsService )
end
2020-04-08 14:13:33 +05:30
# TODO: this method and compare_test_reports use the same
# result type, which is handled by the controller's #reports_response.
# we should minimize mistakes by isolating the common parts.
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
def find_coverage_reports
unless has_coverage_reports?
return { status : :error , status_reason : 'This merge request does not have coverage reports' }
end
compare_reports ( Ci :: GenerateCoverageReportsService )
end
2021-03-11 19:13:27 +05:30
def has_codequality_mr_diff_report?
actual_head_pipeline & . has_codequality_mr_diff_report?
end
# TODO: this method and compare_test_reports use the same
# result type, which is handled by the controller's #reports_response.
# we should minimize mistakes by isolating the common parts.
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
def find_codequality_mr_diff_reports
unless has_codequality_mr_diff_report?
return { status : :error , status_reason : 'This merge request does not have codequality mr diff reports' }
end
compare_reports ( Ci :: GenerateCodequalityMrDiffReportService )
end
2021-02-22 17:27:13 +05:30
def has_codequality_reports?
actual_head_pipeline & . has_reports? ( Ci :: JobArtifact . codequality_reports )
end
def compare_codequality_reports
unless has_codequality_reports?
return { status : :error , status_reason : _ ( 'This merge request does not have codequality reports' ) }
end
compare_reports ( Ci :: CompareCodequalityReportsService )
end
2020-05-24 23:13:21 +05:30
def find_terraform_reports
unless has_terraform_reports?
return { status : :error , status_reason : 'This merge request does not have terraform reports' }
end
2019-12-26 22:10:19 +05:30
2020-05-24 23:13:21 +05:30
compare_reports ( Ci :: GenerateTerraformReportsService )
end
def has_exposed_artifacts?
2019-12-26 22:10:19 +05:30
actual_head_pipeline & . has_exposed_artifacts?
end
# TODO: this method and compare_test_reports use the same
# result type, which is handled by the controller's #reports_response.
# we should minimize mistakes by isolating the common parts.
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
def find_exposed_artifacts
unless has_exposed_artifacts?
return { status : :error , status_reason : 'This merge request does not have exposed artifacts' }
end
compare_reports ( Ci :: GenerateExposedArtifactsReportService )
end
# TODO: consider renaming this as with exposed artifacts we generate reports,
# not always compare
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
2020-07-28 23:09:34 +05:30
def compare_reports ( service_class , current_user = nil , report_type = nil )
with_reactive_cache ( service_class . name , current_user & . id , report_type ) do | data |
unless service_class . new ( project , current_user , id : id , report_type : report_type )
2021-04-17 20:07:23 +05:30
. latest? ( comparison_base_pipeline ( service_class . name ) , actual_head_pipeline , data )
2018-11-18 11:00:15 +05:30
raise InvalidateReactiveCache
end
data
end || { status : :parsing }
end
2021-03-11 19:13:27 +05:30
def has_sast_reports?
! ! actual_head_pipeline & . has_reports? ( :: Ci :: JobArtifact . sast_reports )
end
def has_secret_detection_reports?
! ! actual_head_pipeline & . has_reports? ( :: Ci :: JobArtifact . secret_detection_reports )
end
def compare_sast_reports ( current_user )
return missing_report_error ( " SAST " ) unless has_sast_reports?
compare_reports ( :: Ci :: CompareSecurityReportsService , current_user , 'sast' )
end
def compare_secret_detection_reports ( current_user )
return missing_report_error ( " secret detection " ) unless has_secret_detection_reports?
compare_reports ( :: Ci :: CompareSecurityReportsService , current_user , 'secret_detection' )
end
2020-07-28 23:09:34 +05:30
def calculate_reactive_cache ( identifier , current_user_id = nil , report_type = nil , * args )
2019-02-15 15:39:39 +05:30
service_class = identifier . constantize
2019-12-26 22:10:19 +05:30
# TODO: the type check should change to something that includes exposed artifacts service
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
2019-02-15 15:39:39 +05:30
raise NameError , service_class unless service_class < Ci :: CompareReportsBaseService
2019-12-04 20:38:33 +05:30
current_user = User . find_by ( id : current_user_id )
2021-04-17 20:07:23 +05:30
service_class . new ( project , current_user , id : id , report_type : report_type ) . execute ( comparison_base_pipeline ( identifier ) , actual_head_pipeline )
2018-11-18 11:00:15 +05:30
end
2021-11-18 22:05:49 +05:30
def recent_diff_head_shas ( limit = 100 )
merge_request_diffs . recent ( limit ) . pluck ( :head_commit_sha )
end
2018-03-17 18:26:18 +05:30
def all_commits
MergeRequestDiffCommit
2019-10-12 21:52:04 +05:30
. where ( merge_request_diff : merge_request_diffs . recent )
2018-03-17 18:26:18 +05:30
. limit ( 10_000 )
end
2016-09-29 09:46:39 +05:30
# Note that this could also return SHA from now dangling commits
2016-11-03 12:29:30 +05:30
#
2017-09-10 17:25:29 +05:30
def all_commit_shas
2018-03-17 18:26:18 +05:30
@all_commit_shas || = begin
return commit_shas unless persisted?
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
all_commits . pluck ( :sha ) . uniq
2016-11-03 12:29:30 +05:30
end
2016-09-13 17:45:13 +05:30
end
2016-04-02 18:10:28 +05:30
def merge_commit
@merge_commit || = project . commit ( merge_commit_sha ) if merge_commit_sha
end
2018-10-15 14:42:47 +05:30
def short_merge_commit_sha
Commit . truncate_sha ( merge_commit_sha ) if merge_commit_sha
end
2020-11-24 15:15:51 +05:30
def merged_commit_sha
return unless merged?
sha = merge_commit_sha || squash_commit_sha || diff_head_sha
sha . presence
end
def short_merged_commit_sha
if sha = merged_commit_sha
Commit . truncate_sha ( sha )
end
end
2017-01-15 13:20:01 +05:30
def can_be_reverted? ( current_user )
2018-03-17 18:26:18 +05:30
return false unless merge_commit
2018-11-18 11:00:15 +05:30
return false unless merged_at
2018-03-17 18:26:18 +05:30
2018-11-18 11:00:15 +05:30
# 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
2018-03-17 18:26:18 +05:30
2018-11-18 11:00:15 +05:30
notes_association = notes_with_associations . where ( 'created_at >= ?' , cutoff )
2018-03-17 18:26:18 +05:30
! merge_commit . has_been_reverted? ( current_user , notes_association )
2016-04-02 18:10:28 +05:30
end
2016-06-02 11:05:42 +05:30
2018-11-18 11:00:15 +05:30
def merged_at
strong_memoize ( :merged_at ) do
next unless merged?
metrics & . merged_at ||
merge_event & . created_at ||
2021-01-03 14:25:43 +05:30
resource_state_events . find_by ( state : :merged ) & . created_at ||
2018-11-18 11:00:15 +05:30
notes . system . reorder ( nil ) . find_by ( note : 'merged' ) & . created_at
end
end
2016-06-02 11:05:42 +05:30
def can_be_cherry_picked?
2017-08-17 22:00:37 +05:30
merge_commit . present?
2016-06-02 11:05:42 +05:30
end
2016-08-24 12:49:21 +05:30
2016-09-13 17:45:13 +05:30
def has_complete_diff_refs?
2017-09-10 17:25:29 +05:30
diff_refs && diff_refs . complete?
2016-08-24 12:49:21 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2017-09-10 17:25:29 +05:30
def update_diff_discussion_positions ( old_diff_refs : , new_diff_refs : , current_user : nil )
2016-09-13 17:45:13 +05:30
return unless has_complete_diff_refs?
2016-08-24 12:49:21 +05:30
return if new_diff_refs == old_diff_refs
2017-09-10 17:25:29 +05:30
active_diff_discussions = self . notes . new_diff_notes . discussions . select do | discussion |
discussion . active? ( old_diff_refs )
2016-08-24 12:49:21 +05:30
end
2017-09-10 17:25:29 +05:30
return if active_diff_discussions . empty?
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
paths = active_diff_discussions . flat_map { | n | n . diff_file . paths } . uniq
2016-08-24 12:49:21 +05:30
2022-04-04 11:22:00 +05:30
active_discussions_resolved = active_diff_discussions . all? ( & :resolved? )
2017-09-10 17:25:29 +05:30
service = Discussions :: UpdateDiffPositionService . new (
2016-08-24 12:49:21 +05:30
self . project ,
2017-09-10 17:25:29 +05:30
current_user ,
2016-08-24 12:49:21 +05:30
old_diff_refs : old_diff_refs ,
new_diff_refs : new_diff_refs ,
paths : paths
)
2017-09-10 17:25:29 +05:30
active_diff_discussions . each do | discussion |
service . execute ( discussion )
2022-04-04 11:22:00 +05:30
discussion . clear_memoized_values
2016-08-24 12:49:21 +05:30
end
2018-03-17 18:26:18 +05:30
2022-04-04 11:22:00 +05:30
# If they were all already resolved, this method will have already been called.
# If they all don't get resolved, we don't need to call the method
# If they go from unresolved -> resolved, then we call the method
if ! active_discussions_resolved &&
active_diff_discussions . all? ( & :resolved? ) &&
project . resolve_outdated_diff_discussions?
2018-03-17 18:26:18 +05:30
MergeRequests :: ResolvedDiscussionNotificationService
2021-06-08 01:23:25 +05:30
. new ( project : project , current_user : current_user )
2018-03-17 18:26:18 +05:30
. execute ( self )
end
2016-08-24 12:49:21 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2016-08-24 12:49:21 +05:30
def keep_around_commit
project . repository . keep_around ( self . merge_commit_sha )
end
2016-09-13 17:45:13 +05:30
2017-08-17 22:00:37 +05:30
def has_commits?
2020-04-08 14:13:33 +05:30
merge_request_diff . persisted? && commits_count . to_i > 0
2016-09-13 17:45:13 +05:30
end
2017-08-17 22:00:37 +05:30
def has_no_commits?
! has_commits?
2016-09-13 17:45:13 +05:30
end
2020-01-01 13:55:28 +05:30
def pipeline_coverage_delta
if base_pipeline & . coverage && head_pipeline & . coverage
2022-01-26 12:08:38 +05:30
head_pipeline . coverage - base_pipeline . coverage
2020-01-01 13:55:28 +05:30
end
end
2021-04-17 20:07:23 +05:30
def use_merge_base_pipeline_for_comparison? ( service_class )
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON [ service_class ] & . call ( project )
end
def comparison_base_pipeline ( service_class )
( use_merge_base_pipeline_for_comparison? ( service_class ) && merge_base_pipeline ) || base_pipeline
end
2018-11-18 11:00:15 +05:30
def base_pipeline
2019-02-15 15:39:39 +05:30
@base_pipeline || = project . ci_pipelines
2018-11-18 11:00:15 +05:30
. order ( id : :desc )
2019-07-07 11:18:12 +05:30
. find_by ( sha : diff_base_sha , ref : target_branch )
2018-11-18 11:00:15 +05:30
end
2021-01-03 14:25:43 +05:30
def merge_base_pipeline
@merge_base_pipeline || = project . ci_pipelines
. order ( id : :desc )
. find_by ( sha : actual_head_pipeline . target_sha , ref : target_branch )
end
2018-11-08 19:23:39 +05:30
def discussions_rendered_on_frontend?
true
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def update_project_counter_caches
Projects :: OpenMergeRequestsCountService . new ( target_project ) . refresh_cache
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def first_contribution?
return false if project . team . max_member_access ( author_id ) > Gitlab :: Access :: GUEST
2020-11-24 15:15:51 +05:30
! project . merge_requests . merged . exists? ( author_id : author_id )
2018-03-17 18:26:18 +05:30
end
2018-03-27 19:54:05 +05:30
2018-11-08 19:23:39 +05:30
# TODO: remove once production database rename completes
2019-12-04 20:38:33 +05:30
# https://gitlab.com/gitlab-org/gitlab-foss/issues/47592
2018-11-08 19:23:39 +05:30
alias_attribute :allow_collaboration , :allow_maintainer_to_push
def allow_collaboration
collaborative_push_possible? && allow_maintainer_to_push
2018-03-27 19:54:05 +05:30
end
2018-11-08 19:23:39 +05:30
alias_method :allow_collaboration? , :allow_collaboration
2018-03-27 19:54:05 +05:30
2018-11-08 19:23:39 +05:30
def collaborative_push_possible?
2018-03-27 19:54:05 +05:30
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
2018-11-08 19:23:39 +05:30
def can_allow_collaboration? ( user )
collaborative_push_possible? &&
2018-03-27 19:54:05 +05:30
Ability . allowed? ( user , :push_code , source_project )
end
2018-11-08 19:23:39 +05:30
2019-02-15 15:39:39 +05:30
def find_actual_head_pipeline
2022-06-21 17:19:12 +05:30
all_pipelines . for_sha_or_source_sha ( diff_head_sha ) . first
2019-07-07 11:18:12 +05:30
end
2020-01-01 13:55:28 +05:30
def etag_caching_enabled?
true
end
def recent_visible_deployments
deployments . visible . includes ( :environment ) . order ( id : :desc ) . limit ( 10 )
end
2020-06-23 00:09:42 +05:30
def banzai_render_context ( field )
super . merge ( label_url_method : :project_merge_requests_url )
end
2020-07-28 23:09:34 +05:30
override :ensure_metrics
def ensure_metrics
2022-01-26 12:08:38 +05:30
MergeRequest :: Metrics . record! ( self )
2020-07-28 23:09:34 +05:30
end
2020-11-24 15:15:51 +05:30
def allows_reviewers?
2021-03-11 19:13:27 +05:30
true
2020-11-24 15:15:51 +05:30
end
2021-01-03 14:25:43 +05:30
def allows_multiple_reviewers?
false
end
2021-03-08 18:12:59 +05:30
def supports_assignee?
true
end
2021-12-11 22:18:48 +05:30
def find_assignee ( user )
merge_request_assignees . find_by ( user_id : user . id )
end
2022-05-07 20:08:51 +05:30
def merge_request_assignees_with ( user_ids )
merge_request_assignees . where ( user_id : user_ids )
end
2021-03-11 19:13:27 +05:30
def find_reviewer ( user )
merge_request_reviewers . find_by ( user_id : user . id )
end
2022-05-07 20:08:51 +05:30
def merge_request_reviewers_with ( user_ids )
merge_request_reviewers . where ( user_id : user_ids )
end
2021-03-11 19:13:27 +05:30
def enabled_reports
{
sast : report_type_enabled? ( :sast ) ,
secret_detection : report_type_enabled? ( :secret_detection )
}
end
2021-04-17 20:07:23 +05:30
def includes_ci_config?
return false unless diff_stats
diff_stats . map ( & :path ) . include? ( project . ci_config_path_or_default )
end
2021-06-08 01:23:25 +05:30
def context_commits_diff
strong_memoize ( :context_commits_diff ) do
ContextCommitsDiff . new ( self )
end
end
2021-12-11 22:18:48 +05:30
def attention_requested_enabled?
Feature . enabled? ( :mr_attention_requests , project , default_enabled : :yaml )
end
2019-07-31 22:56:46 +05:30
private
2022-05-07 20:08:51 +05:30
attr_accessor :skip_fetch_ref
2021-09-04 01:27:46 +05:30
def set_draft_status
self . draft = draft?
end
2021-03-11 19:13:27 +05:30
def missing_report_error ( report_type )
{ status : :error , status_reason : " This merge request does not have #{ report_type } reports " }
end
2019-12-26 22:10:19 +05:30
def with_rebase_lock
2021-04-17 20:07:23 +05:30
with_retried_nowait_lock { yield }
2019-12-26 22:10:19 +05:30
end
# If the merge request is idle in transaction or has a SELECT FOR
# UPDATE, we don't want to block indefinitely or this could cause a
# queue of SELECT FOR UPDATE calls. Instead, try to get the lock for
# 5 s before raising an error to the user.
def with_retried_nowait_lock
# Try at most 0.25 + (1.5 * .25) + (1.5^2 * .25) ... (1.5^5 * .25) = 5.2 s to get the lock
Retriable . retriable ( on : ActiveRecord :: LockWaitTimeout , tries : 6 , base_interval : 0 . 25 ) do
with_lock ( 'FOR UPDATE NOWAIT' ) do
yield
end
end
rescue ActiveRecord :: LockWaitTimeout = > e
2020-01-01 13:55:28 +05:30
Gitlab :: ErrorTracking . track_exception ( e )
2020-03-13 15:44:24 +05:30
raise RebaseLockTimeout , _ ( 'Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later.' )
2019-12-26 22:10:19 +05:30
end
2019-07-07 11:18:12 +05:30
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
2019-02-15 15:39:39 +05:30
end
2019-10-12 21:52:04 +05:30
def expire_etag_cache
return unless project . namespace
key = Gitlab :: Routing . url_helpers . cached_widget_project_json_merge_request_path ( project , self , format : :json )
Gitlab :: EtagCaching :: Store . new . touch ( key )
end
2021-03-11 19:13:27 +05:30
def report_type_enabled? ( report_type )
! ! actual_head_pipeline & . batch_lookup_report_artifact_for_file_type ( report_type )
end
2014-09-02 18:07:02 +05:30
end
2020-01-01 13:55:28 +05:30
2021-06-08 01:23:25 +05:30
MergeRequest . prepend_mod_with ( 'MergeRequest' )