2012-11-19 13:24:05 -05:00
# == Schema Information
#
# Table name: merge_requests
#
2013-08-21 05:34:02 -04: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 08:05:03 -04:00
# created_at :datetime
# updated_at :datetime
2013-08-21 05:34:02 -04:00
# milestone_id :integer
# state :string(255)
# merge_status :string(255)
# target_project_id :integer not null
# iid :integer
2013-10-01 08:15:28 -04:00
# description :text
2014-08-25 05:25:02 -04:00
# position :integer default(0)
2015-01-22 12:40:03 -05:00
# locked_at :datetime
2015-09-06 10:48:48 -04:00
# updated_by_id :integer
2013-03-15 09:16:02 -04:00
#
2012-11-19 13:24:05 -05:00
2012-09-26 14:52:01 -04:00
require Rails . root . join ( " app/models/commit " )
2013-01-02 17:01:08 -05:00
require Rails . root . join ( " lib/static_model " )
2012-03-14 18:57:43 -04:00
2011-11-28 02:39:43 -05:00
class MergeRequest < ActiveRecord :: Base
2013-08-21 05:16:26 -04:00
include InternalId
2015-05-02 23:11:21 -04:00
include Issuable
include Referable
2015-02-05 19:49:41 -05:00
include Sortable
2015-05-02 23:11:21 -04:00
include Taskable
2012-06-07 08:44:57 -04:00
2013-07-16 17:14:03 -04: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 13:22:20 -05:00
2014-01-22 08:20:20 -05:00
has_one :merge_request_diff , dependent : :destroy
2014-02-18 13:17:26 -05:00
2014-01-22 13:22:20 -05:00
after_create :create_merge_request_diff
2014-02-18 13:17:26 -05:00
after_update :update_merge_request_diff
2014-01-22 08:20:20 -05:00
delegate :commits , :diffs , :last_commit , :last_commit_short_sha , to : :merge_request_diff , prefix : nil
2013-04-25 10:15:33 -04:00
2013-12-12 05:20:54 -05: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 08:34:06 -04:00
# Temporary fields to store compare vars
# when creating new merge request
2014-07-28 13:54:40 -04:00
attr_accessor :can_be_created , :compare_failed ,
2014-07-15 11:28:21 -04:00
:compare_commits , :compare_diffs
2014-07-15 08:34:06 -04:00
2013-02-18 08:22:18 -05:00
state_machine :state , initial : :opened do
2013-02-18 03:40:56 -05:00
event :close do
transition [ :reopened , :opened ] = > :closed
end
2015-08-11 08:33:31 -04:00
event :mark_as_merged do
2014-01-24 07:18:32 -05:00
transition [ :reopened , :opened , :locked ] = > :merged
2013-02-18 03:40:56 -05:00
end
event :reopen do
2013-02-18 08:22:18 -05:00
transition closed : :reopened
2013-02-18 03:40:56 -05:00
end
2014-06-26 08:09:17 -04:00
event :lock_mr do
2014-01-24 07:18:32 -05:00
transition [ :reopened , :opened ] = > :locked
end
2014-06-26 08:09:17 -04:00
event :unlock_mr do
2014-01-24 07:18:32 -05:00
transition locked : :reopened
end
2014-12-05 08:49:25 -05:00
after_transition any = > :locked do | merge_request , transition |
merge_request . locked_at = Time . now
merge_request . save
end
2015-02-02 22:30:09 -05:00
after_transition locked : ( any - :locked ) do | merge_request , transition |
2014-12-05 08:49:25 -05:00
merge_request . locked_at = nil
merge_request . save
end
2013-02-18 03:40:56 -05:00
state :opened
state :reopened
state :closed
state :merged
2014-01-24 07:18:32 -05:00
state :locked
2013-02-18 03:40:56 -05:00
end
2013-02-20 08:15:01 -05: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 16:51:16 -04:00
transition [ :unchecked , :cannot_be_merged ] = > :can_be_merged
2013-02-20 08:15:01 -05:00
end
event :mark_as_unmergeable do
2015-03-19 16:51:16 -04:00
transition [ :unchecked , :can_be_merged ] = > :cannot_be_merged
2013-02-20 08:15:01 -05:00
end
2013-02-26 03:38:40 -05:00
state :unchecked
2013-02-20 08:15:01 -05:00
state :can_be_merged
state :cannot_be_merged
2015-03-23 12:30:19 -04: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 08:15:01 -05:00
end
2012-03-30 01:05:04 -04:00
2013-12-12 05:20:54 -05:00
validates :source_project , presence : true , unless : :allow_broken
2012-10-08 20:10:04 -04:00
validates :source_branch , presence : true
2013-04-25 10:15:33 -04:00
validates :target_project , presence : true
2012-10-08 20:10:04 -04:00
validates :target_branch , presence : true
2013-04-25 10:15:33 -04:00
validate :validate_branches
2014-04-02 14:51:53 -04:00
validate :validate_fork
2011-11-28 02:39:43 -05:00
2013-06-06 17:22:36 -04: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 10:15:33 -04:00
scope :by_branch , - > ( branch_name ) { where ( " (source_branch LIKE :branch) OR (target_branch LIKE :branch) " , branch : branch_name ) }
2013-02-20 08:37:20 -05:00
scope :cared , - > ( user ) { where ( 'assignee_id = :user OR author_id = :user' , user : user . id ) }
2013-02-26 03:38:40 -05:00
scope :by_milestone , - > ( milestone ) { where ( milestone_id : milestone ) }
2013-04-25 10:15:33 -04: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 10:29:31 -04:00
scope :of_projects , - > ( ids ) { where ( target_project_id : ids ) }
2015-06-18 13:15:17 -04:00
scope :merged , - > { with_state ( :merged ) }
scope :closed , - > { with_state ( :closed ) }
scope :closed_and_merged , - > { with_states ( :closed , :merged ) }
2013-02-21 07:11:24 -05:00
2015-05-02 23:11:21 -04:00
def self . reference_prefix
'!'
end
2015-05-14 16:59:39 -04: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 16:10:55 -04:00
( #{Project.reference_pattern})?
2015-05-14 16:59:39 -04:00
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
} x
end
2015-05-02 23:11:21 -04: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 17:54:49 -04:00
def validate_branches
2014-01-22 08:20:20 -05:00
if target_project == source_project && target_branch == source_branch
2013-04-25 10:15:33 -04:00
errors . add :branch_conflict , " You can not use same project/branch for source and target "
2012-03-13 17:54:49 -04:00
end
2013-06-14 08:03:22 -04:00
2013-06-14 10:19:26 -04:00
if opened? || reopened?
2013-06-26 16:45:57 -04: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 10:19:26 -04:00
similar_mrs = similar_mrs . where ( 'id not in (?)' , self . id ) if self . id
if similar_mrs . any?
2014-08-18 14:09:09 -04:00
errors . add :validate_branches ,
" Cannot Create: This merge request already exists: #{
similar_mrs . pluck ( :title )
} "
2013-06-14 10:19:26 -04:00
end
2013-06-14 08:03:22 -04:00
end
2012-03-13 17:54:49 -04:00
end
2014-04-02 14:51:53 -04:00
def validate_fork
2014-04-03 03:36:10 -04:00
return true unless target_project && source_project
if target_project == source_project
2014-04-02 14:51:53 -04: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 14:09:09 -04:00
errors . add :validate_fork ,
'Source project is not a fork of target project'
2014-04-02 14:51:53 -04:00
end
end
end
2014-02-18 13:17:26 -05:00
def update_merge_request_diff
if source_branch_changed? || target_branch_changed?
reload_code
end
end
2012-03-15 19:45:46 -04:00
def reload_code
2014-03-11 17:50:02 -04:00
if merge_request_diff && open ?
2014-01-24 07:38:02 -05:00
merge_request_diff . reload_content
end
2012-03-15 19:45:46 -04:00
end
2012-03-30 01:05:04 -04:00
def check_if_can_be_merged
2015-08-11 08:33:31 -04:00
can_be_merged =
project . repository . can_be_merged? ( source_sha , target_branch )
if can_be_merged
2013-02-20 08:15:01 -05:00
mark_as_mergeable
else
mark_as_unmergeable
end
2012-03-29 17:27:42 -04:00
end
2012-03-15 17:32:00 -04:00
def merge_event
2013-04-25 10:15:33 -04:00
self . target_project . events . where ( target_id : self . id , target_type : " MergeRequest " , action : Event :: MERGED ) . last
2012-03-15 17:32:00 -04:00
end
2012-03-15 19:45:46 -04:00
def closed_event
2013-04-25 10:15:33 -04:00
self . target_project . events . where ( target_id : self . id , target_type : " MergeRequest " , action : Event :: CLOSED ) . last
2012-03-15 19:45:46 -04:00
end
2014-02-28 15:43:16 -05:00
def open?
opened? || reopened?
end
2015-04-30 09:43:32 -04:00
def work_in_progress?
title =~ / \ A \ [?WIP \ ]?:? /i
end
2015-08-11 08:33:31 -04:00
def mergeable?
2015-04-30 09:43:32 -04:00
open ? && ! work_in_progress? && can_be_merged?
end
2015-08-11 08:33:31 -04:00
def gitlab_merge_status
2015-04-30 09:43:32 -04:00
if work_in_progress?
" work_in_progress "
else
merge_status_name
end
end
2012-10-04 18:25:40 -04:00
def mr_and_commit_notes
2014-01-22 08:54:53 -05: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 09:38:14 -04: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 07:49:01 -04:00
mr_id : id ,
2015-04-21 09:38:14 -04:00
commit_ids : commit_ids ,
target_project_id : target_project_id ,
source_project_id : source_project_id
2013-10-07 07:49:01 -04:00
)
2012-10-04 18:25:40 -04:00
end
2013-06-06 17:22:36 -04:00
2012-11-22 18:55:57 -05:00
# Returns the raw diff for this merge request
#
# see "git diff"
2013-04-25 10:15:33 -04:00
def to_diff ( current_user )
2015-08-11 08:33:31 -04:00
target_project . repository . diff_text ( target_branch , source_sha )
2012-11-22 18:55:57 -05:00
end
# Returns the commit as a series of email patches.
#
# see "git format-patch"
2013-04-25 10:15:33 -04:00
def to_patch ( current_user )
2015-08-11 08:33:31 -04:00
target_project . repository . format_patch ( target_branch , source_sha )
2012-11-22 18:55:57 -05:00
end
2012-12-10 22:14:05 -05:00
2014-09-15 03:10:35 -04:00
def hook_attrs
attrs = {
source : source_project . hook_attrs ,
target : target_project . hook_attrs ,
last_commit : nil
}
unless last_commit . nil?
2015-04-21 09:15:49 -04:00
attrs . merge! ( last_commit : last_commit . hook_attrs )
2014-09-15 03:10:35 -04:00
end
attributes . merge! ( attrs )
end
2013-04-25 10:15:33 -04:00
def for_fork?
target_project != source_project
end
2013-08-19 15:10:56 -04:00
def project
target_project
end
2013-05-30 19:16:49 -04:00
# Return the set of issues that will be closed if this merge request is accepted.
2015-03-27 07:19:48 -04:00
def closes_issues ( current_user = self . author )
2013-05-30 19:16:49 -04:00
if target_branch == project . default_branch
2015-04-21 09:15:49 -04:00
issues = commits . flat_map { | c | c . closes_issues ( current_user ) }
2015-04-03 12:03:26 -04:00
issues . push ( * Gitlab :: ClosingIssueExtractor . new ( project , current_user ) .
closed_by_message ( description ) )
2014-06-13 10:19:08 -04:00
issues . uniq . sort_by ( & :id )
2013-05-30 19:16:49 -04:00
else
[ ]
end
end
2013-12-12 04:34:42 -05: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 07:36:58 -05:00
def source_project_namespace
if source_project && source_project . namespace
source_project . namespace . path
else
" (removed) "
end
end
2014-05-08 08:37:36 -04:00
def target_project_namespace
if target_project && target_project . namespace
target_project . namespace . path
else
" (removed) "
end
end
2013-12-12 04:34:42 -05: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 14:40:45 -05: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 14:44:24 -04:00
Event . reset_event_cache_for ( self )
2013-12-13 14:40:45 -05:00
end
2014-01-13 10:20:30 -05:00
def merge_commit_message
message = " Merge branch ' #{ source_branch } ' into ' #{ target_branch } ' "
message << " \n \n "
2014-01-14 03:47:28 -05:00
message << title . to_s
2014-01-13 10:20:30 -05:00
message << " \n \n "
2014-01-14 03:47:28 -05:00
message << description . to_s
2014-07-01 15:54:19 -04:00
message << " \n \n "
message << " See merge request ! #{ iid } "
2014-01-14 03:47:28 -05:00
message
2014-01-13 10:20:30 -05:00
end
2014-01-31 06:24:38 -05:00
# Return array of possible target branches
2015-01-18 10:29:37 -05:00
# depends on target project of MR
2014-01-31 06:24:38 -05: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 10:29:37 -05:00
# depends on source project of MR
2014-01-31 06:24:38 -05:00
def source_branches
if source_project . nil?
[ ]
else
source_project . repository . branch_names
end
end
2014-12-05 09:02:08 -05:00
def locked_long_ago?
2015-04-15 02:11:13 -04:00
return false unless locked?
locked_at . nil? || locked_at < ( Time . now - 1 . day )
2014-12-05 09:02:08 -05:00
end
2015-06-11 03:40:26 -04:00
def has_ci?
source_project . ci_service && commits . any?
end
def branch_missing?
! source_branch_exists? || ! target_branch_exists?
end
2015-06-11 12:45:12 -04:00
def can_be_merged_by? ( user )
:: Gitlab :: GitAccess . new ( user , project ) . can_push_to_branch? ( target_branch )
end
2015-06-18 13:15:17 -04:00
def state_human_name
if merged?
" Merged "
elsif closed?
" Closed "
else
" Open "
end
end
2015-08-11 08:33:31 -04: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 10:25:59 -04:00
ref_path
2015-08-11 08:33:31 -04:00
)
end
2015-09-21 10:25:59 -04: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 08:33:31 -04:00
def in_locked_state
begin
lock_mr
yield
ensure
unlock_mr if locked?
end
end
2011-11-28 02:39:43 -05:00
end