2012-11-19 18:24:05 +00:00
# == Schema Information
#
# Table name: merge_requests
#
2013-08-21 09:34:02 +00:00
# id :integer not null, primary key
# target_branch :string(255) not null
# source_branch :string(255) not null
# source_project_id :integer not null
# author_id :integer
# assignee_id :integer
# title :string(255)
2014-04-09 12:05:03 +00:00
# created_at :datetime
# updated_at :datetime
2013-08-21 09:34:02 +00:00
# milestone_id :integer
# state :string(255)
# merge_status :string(255)
# target_project_id :integer not null
# iid :integer
2013-10-01 12:15:28 +00:00
# description :text
2014-08-25 09:25:02 +00:00
# position :integer default(0)
2015-01-22 17:40:03 +00:00
# locked_at :datetime
2015-09-06 14:48:48 +00:00
# updated_by_id :integer
2013-03-15 13:16:02 +00:00
#
2012-11-19 18:24:05 +00:00
2012-09-26 18:52:01 +00:00
require Rails . root . join ( " app/models/commit " )
2013-01-02 22:01:08 +00:00
require Rails . root . join ( " lib/static_model " )
2012-03-14 22:57:43 +00:00
2011-11-28 07:39:43 +00:00
class MergeRequest < ActiveRecord :: Base
2013-08-21 09:16:26 +00:00
include InternalId
2015-05-03 03:11:21 +00:00
include Issuable
include Referable
2015-02-06 00:49:41 +00:00
include Sortable
2015-05-03 03:11:21 +00:00
include Taskable
2012-06-07 12:44:57 +00:00
2013-07-16 21:14:03 +00:00
belongs_to :target_project , foreign_key : :target_project_id , class_name : " Project "
belongs_to :source_project , foreign_key : :source_project_id , class_name : " Project "
2014-01-22 18:22:20 +00:00
2014-01-22 13:20:20 +00:00
has_one :merge_request_diff , dependent : :destroy
2014-02-18 18:17:26 +00:00
2014-01-22 18:22:20 +00:00
after_create :create_merge_request_diff
2014-02-18 18:17:26 +00:00
after_update :update_merge_request_diff
2014-01-22 13:20:20 +00:00
delegate :commits , :diffs , :last_commit , :last_commit_short_sha , to : :merge_request_diff , prefix : nil
2013-04-25 14:15:33 +00:00
2013-12-12 10:20:54 +00:00
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
2014-07-15 12:34:06 +00:00
# Temporary fields to store compare vars
# when creating new merge request
2014-07-28 17:54:40 +00:00
attr_accessor :can_be_created , :compare_failed ,
2014-07-15 15:28:21 +00:00
:compare_commits , :compare_diffs
2014-07-15 12:34:06 +00:00
2013-02-18 13:22:18 +00:00
state_machine :state , initial : :opened do
2013-02-18 08:40:56 +00:00
event :close do
transition [ :reopened , :opened ] = > :closed
end
2015-08-11 12:33:31 +00:00
event :mark_as_merged do
2014-01-24 12:18:32 +00:00
transition [ :reopened , :opened , :locked ] = > :merged
2013-02-18 08:40:56 +00:00
end
event :reopen do
2013-02-18 13:22:18 +00:00
transition closed : :reopened
2013-02-18 08:40:56 +00:00
end
2014-06-26 12:09:17 +00:00
event :lock_mr do
2014-01-24 12:18:32 +00:00
transition [ :reopened , :opened ] = > :locked
end
2014-06-26 12:09:17 +00:00
event :unlock_mr do
2014-01-24 12:18:32 +00:00
transition locked : :reopened
end
2014-12-05 13:49:25 +00:00
after_transition any = > :locked do | merge_request , transition |
merge_request . locked_at = Time . now
merge_request . save
end
2015-02-03 03:30:09 +00:00
after_transition locked : ( any - :locked ) do | merge_request , transition |
2014-12-05 13:49:25 +00:00
merge_request . locked_at = nil
merge_request . save
end
2013-02-18 08:40:56 +00:00
state :opened
state :reopened
state :closed
state :merged
2014-01-24 12:18:32 +00:00
state :locked
2013-02-18 08:40:56 +00:00
end
2013-02-20 13:15:01 +00:00
state_machine :merge_status , initial : :unchecked do
event :mark_as_unchecked do
transition [ :can_be_merged , :cannot_be_merged ] = > :unchecked
end
event :mark_as_mergeable do
2015-03-19 20:51:16 +00:00
transition [ :unchecked , :cannot_be_merged ] = > :can_be_merged
2013-02-20 13:15:01 +00:00
end
event :mark_as_unmergeable do
2015-03-19 20:51:16 +00:00
transition [ :unchecked , :can_be_merged ] = > :cannot_be_merged
2013-02-20 13:15:01 +00:00
end
2013-02-26 08:38:40 +00:00
state :unchecked
2013-02-20 13:15:01 +00:00
state :can_be_merged
state :cannot_be_merged
2015-03-23 16:30:19 +00:00
around_transition do | merge_request , transition , block |
merge_request . record_timestamps = false
begin
block . call
ensure
merge_request . record_timestamps = true
end
end
2013-02-20 13:15:01 +00:00
end
2012-03-30 05:05:04 +00:00
2013-12-12 10:20:54 +00:00
validates :source_project , presence : true , unless : :allow_broken
2012-10-09 00:10:04 +00:00
validates :source_branch , presence : true
2013-04-25 14:15:33 +00:00
validates :target_project , presence : true
2012-10-09 00:10:04 +00:00
validates :target_branch , presence : true
2013-04-25 14:15:33 +00:00
validate :validate_branches
2014-04-02 18:51:53 +00:00
validate :validate_fork
2011-11-28 07:39:43 +00:00
2013-06-06 21:22:36 +00:00
scope :of_group , - > ( group ) { where ( " source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids) " , group_project_ids : group . project_ids ) }
2013-04-25 14:15:33 +00:00
scope :by_branch , - > ( branch_name ) { where ( " (source_branch LIKE :branch) OR (target_branch LIKE :branch) " , branch : branch_name ) }
2013-02-20 13:37:20 +00:00
scope :cared , - > ( user ) { where ( 'assignee_id = :user OR author_id = :user' , user : user . id ) }
2013-02-26 08:38:40 +00:00
scope :by_milestone , - > ( milestone ) { where ( milestone_id : milestone ) }
2013-04-25 14:15:33 +00:00
scope :in_projects , - > ( project_ids ) { where ( " source_project_id in (:project_ids) OR target_project_id in (:project_ids) " , project_ids : project_ids ) }
2013-08-08 14:29:31 +00:00
scope :of_projects , - > ( ids ) { where ( target_project_id : ids ) }
2015-06-18 17:15:17 +00:00
scope :merged , - > { with_state ( :merged ) }
scope :closed , - > { with_state ( :closed ) }
scope :closed_and_merged , - > { with_states ( :closed , :merged ) }
2013-02-21 12:11:24 +00:00
2015-05-03 03:11:21 +00:00
def self . reference_prefix
'!'
end
2015-05-14 20:59:39 +00:00
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
def self . reference_pattern
%r{
2015-05-15 20:10:55 +00:00
( #{Project.reference_pattern})?
2015-05-14 20:59:39 +00:00
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
} x
end
2015-05-03 03:11:21 +00:00
def to_reference ( from_project = nil )
reference = " #{ self . class . reference_prefix } #{ iid } "
if cross_project_reference? ( from_project )
reference = project . to_reference + reference
end
reference
end
2012-03-13 21:54:49 +00:00
def validate_branches
2014-01-22 13:20:20 +00:00
if target_project == source_project && target_branch == source_branch
2013-04-25 14:15:33 +00:00
errors . add :branch_conflict , " You can not use same project/branch for source and target "
2012-03-13 21:54:49 +00:00
end
2013-06-14 12:03:22 +00:00
2013-06-14 14:19:26 +00:00
if opened? || reopened?
2013-06-26 20:45:57 +00:00
similar_mrs = self . target_project . merge_requests . where ( source_branch : source_branch , target_branch : target_branch , source_project_id : source_project . id ) . opened
2013-06-14 14:19:26 +00:00
similar_mrs = similar_mrs . where ( 'id not in (?)' , self . id ) if self . id
if similar_mrs . any?
2014-08-18 18:09:09 +00:00
errors . add :validate_branches ,
" Cannot Create: This merge request already exists: #{
similar_mrs . pluck ( :title )
} "
2013-06-14 14:19:26 +00:00
end
2013-06-14 12:03:22 +00:00
end
2012-03-13 21:54:49 +00:00
end
2014-04-02 18:51:53 +00:00
def validate_fork
2014-04-03 07:36:10 +00:00
return true unless target_project && source_project
if target_project == source_project
2014-04-02 18:51:53 +00:00
true
else
# If source and target projects are different
# we should check if source project is actually a fork of target project
if source_project . forked_from? ( target_project )
true
else
2014-08-18 18:09:09 +00:00
errors . add :validate_fork ,
'Source project is not a fork of target project'
2014-04-02 18:51:53 +00:00
end
end
end
2014-02-18 18:17:26 +00:00
def update_merge_request_diff
if source_branch_changed? || target_branch_changed?
reload_code
end
end
2012-03-15 23:45:46 +00:00
def reload_code
2014-03-11 21:50:02 +00:00
if merge_request_diff && open ?
2014-01-24 12:38:02 +00:00
merge_request_diff . reload_content
end
2012-03-15 23:45:46 +00:00
end
2012-03-30 05:05:04 +00:00
def check_if_can_be_merged
2015-08-11 12:33:31 +00:00
can_be_merged =
project . repository . can_be_merged? ( source_sha , target_branch )
if can_be_merged
2013-02-20 13:15:01 +00:00
mark_as_mergeable
else
mark_as_unmergeable
end
2012-03-29 21:27:42 +00:00
end
2012-03-15 21:32:00 +00:00
def merge_event
2013-04-25 14:15:33 +00:00
self . target_project . events . where ( target_id : self . id , target_type : " MergeRequest " , action : Event :: MERGED ) . last
2012-03-15 21:32:00 +00:00
end
2012-03-15 23:45:46 +00:00
def closed_event
2013-04-25 14:15:33 +00:00
self . target_project . events . where ( target_id : self . id , target_type : " MergeRequest " , action : Event :: CLOSED ) . last
2012-03-15 23:45:46 +00:00
end
2014-02-28 20:43:16 +00:00
def open?
opened? || reopened?
end
2015-04-30 13:43:32 +00:00
def work_in_progress?
2015-09-30 21:12:33 +00:00
! ! ( title =~ / \ A \ [?WIP \ ]?:? /i )
2015-04-30 13:43:32 +00:00
end
2015-08-11 12:33:31 +00:00
def mergeable?
2015-04-30 13:43:32 +00:00
open ? && ! work_in_progress? && can_be_merged?
end
2015-08-11 12:33:31 +00:00
def gitlab_merge_status
2015-04-30 13:43:32 +00:00
if work_in_progress?
" work_in_progress "
else
merge_status_name
end
end
2012-10-04 22:25:40 +00:00
def mr_and_commit_notes
2014-01-22 13:54:53 +00:00
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits . last ( commits_for_notes_limit ) . map ( & :id )
2015-04-21 13:38:14 +00:00
Note . where (
" (project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR " +
" (project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids)) " ,
2013-10-07 11:49:01 +00:00
mr_id : id ,
2015-04-21 13:38:14 +00:00
commit_ids : commit_ids ,
target_project_id : target_project_id ,
source_project_id : source_project_id
2013-10-07 11:49:01 +00:00
)
2012-10-04 22:25:40 +00:00
end
2013-06-06 21:22:36 +00:00
2012-11-22 23:55:57 +00:00
# Returns the raw diff for this merge request
#
# see "git diff"
2013-04-25 14:15:33 +00:00
def to_diff ( current_user )
2015-08-11 12:33:31 +00:00
target_project . repository . diff_text ( target_branch , source_sha )
2012-11-22 23:55:57 +00:00
end
# Returns the commit as a series of email patches.
#
# see "git format-patch"
2013-04-25 14:15:33 +00:00
def to_patch ( current_user )
2015-08-11 12:33:31 +00:00
target_project . repository . format_patch ( target_branch , source_sha )
2012-11-22 23:55:57 +00:00
end
2012-12-11 03:14:05 +00:00
2014-09-15 07:10:35 +00:00
def hook_attrs
attrs = {
source : source_project . hook_attrs ,
target : target_project . hook_attrs ,
2015-09-25 20:07:08 +00:00
last_commit : nil ,
work_in_progress : work_in_progress?
2014-09-15 07:10:35 +00:00
}
unless last_commit . nil?
2015-04-21 13:15:49 +00:00
attrs . merge! ( last_commit : last_commit . hook_attrs )
2014-09-15 07:10:35 +00:00
end
attributes . merge! ( attrs )
end
2013-04-25 14:15:33 +00:00
def for_fork?
target_project != source_project
end
2013-08-19 19:10:56 +00:00
def project
target_project
end
2013-05-30 23:16:49 +00:00
# Return the set of issues that will be closed if this merge request is accepted.
2015-03-27 11:19:48 +00:00
def closes_issues ( current_user = self . author )
2013-05-30 23:16:49 +00:00
if target_branch == project . default_branch
2015-04-21 13:15:49 +00:00
issues = commits . flat_map { | c | c . closes_issues ( current_user ) }
2015-04-03 16:03:26 +00:00
issues . push ( * Gitlab :: ClosingIssueExtractor . new ( project , current_user ) .
closed_by_message ( description ) )
2014-06-13 14:19:08 +00:00
issues . uniq . sort_by ( & :id )
2013-05-30 23:16:49 +00:00
else
[ ]
end
end
2013-12-12 09:34:42 +00:00
def target_project_path
if target_project
target_project . path_with_namespace
else
" (removed) "
end
end
def source_project_path
if source_project
source_project . path_with_namespace
else
" (removed) "
end
end
2014-02-15 12:36:58 +00:00
def source_project_namespace
if source_project && source_project . namespace
source_project . namespace . path
else
" (removed) "
end
end
2014-05-08 12:37:36 +00:00
def target_project_namespace
if target_project && target_project . namespace
target_project . namespace . path
else
" (removed) "
end
end
2013-12-12 09:34:42 +00:00
def source_branch_exists?
return false unless self . source_project
self . source_project . repository . branch_names . include? ( self . source_branch )
end
def target_branch_exists?
return false unless self . target_project
self . target_project . repository . branch_names . include? ( self . target_branch )
end
2013-12-13 19:40:45 +00:00
# Reset merge request events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when a merge request is updated
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
2014-07-16 18:44:24 +00:00
Event . reset_event_cache_for ( self )
2013-12-13 19:40:45 +00:00
end
2014-01-13 15:20:30 +00:00
def merge_commit_message
message = " Merge branch ' #{ source_branch } ' into ' #{ target_branch } ' "
message << " \n \n "
2014-01-14 08:47:28 +00:00
message << title . to_s
2014-01-13 15:20:30 +00:00
message << " \n \n "
2014-01-14 08:47:28 +00:00
message << description . to_s
2014-07-01 19:54:19 +00:00
message << " \n \n "
message << " See merge request ! #{ iid } "
2014-01-14 08:47:28 +00:00
message
2014-01-13 15:20:30 +00:00
end
2014-01-31 11:24:38 +00:00
# Return array of possible target branches
2015-01-18 15:29:37 +00:00
# depends on target project of MR
2014-01-31 11:24:38 +00:00
def target_branches
if target_project . nil?
[ ]
else
target_project . repository . branch_names
end
end
# Return array of possible source branches
2015-01-18 15:29:37 +00:00
# depends on source project of MR
2014-01-31 11:24:38 +00:00
def source_branches
if source_project . nil?
[ ]
else
source_project . repository . branch_names
end
end
2014-12-05 14:02:08 +00:00
def locked_long_ago?
2015-04-15 06:11:13 +00:00
return false unless locked?
locked_at . nil? || locked_at < ( Time . now - 1 . day )
2014-12-05 14:02:08 +00:00
end
2015-06-11 07:40:26 +00:00
def has_ci?
source_project . ci_service && commits . any?
end
def branch_missing?
! source_branch_exists? || ! target_branch_exists?
end
2015-06-11 16:45:12 +00:00
def can_be_merged_by? ( user )
:: Gitlab :: GitAccess . new ( user , project ) . can_push_to_branch? ( target_branch )
end
2015-06-18 17:15:17 +00:00
def state_human_name
if merged?
" Merged "
elsif closed?
" Closed "
else
" Open "
end
end
2015-08-11 12:33:31 +00:00
def target_sha
@target_sha || = target_project .
repository . commit ( target_branch ) . sha
end
def source_sha
commits . first . sha
end
def fetch_ref
target_project . repository . fetch_ref (
source_project . repository . path_to_repo ,
" refs/heads/ #{ source_branch } " ,
2015-09-21 14:25:59 +00:00
ref_path
2015-08-11 12:33:31 +00:00
)
end
2015-09-21 14:25:59 +00:00
def ref_path
" refs/merge-requests/ #{ iid } /head "
end
def ref_is_fetched?
File . exists? ( File . join ( project . repository . path_to_repo , ref_path ) )
end
def ensure_ref_fetched
fetch_ref unless ref_is_fetched?
end
2015-08-11 12:33:31 +00:00
def in_locked_state
begin
lock_mr
yield
ensure
unlock_mr if locked?
end
end
2011-11-28 07:39:43 +00:00
end