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
|
2017-03-09 20:29:11 -05:00
|
|
|
include Noteable
|
2015-05-02 23:11:21 -04:00
|
|
|
include Referable
|
2015-02-05 19:49:41 -05:00
|
|
|
include Sortable
|
2017-06-09 14:34:58 -04:00
|
|
|
include IgnorableColumn
|
|
|
|
|
|
|
|
ignore_column :position
|
2012-06-07 08:44:57 -04:00
|
|
|
|
2016-10-18 17:20:36 -04:00
|
|
|
belongs_to :target_project, class_name: "Project"
|
|
|
|
belongs_to :source_project, class_name: "Project"
|
2015-11-02 11:27:38 -05:00
|
|
|
belongs_to :merge_user, class_name: "User"
|
2014-01-22 13:22:20 -05:00
|
|
|
|
2016-07-25 07:10:21 -04:00
|
|
|
has_many :merge_request_diffs, dependent: :destroy
|
2016-08-02 09:28:37 -04:00
|
|
|
has_one :merge_request_diff,
|
|
|
|
-> { order('merge_request_diffs.id DESC') }
|
|
|
|
|
2017-03-17 18:14:27 -04:00
|
|
|
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
|
|
|
|
|
2016-07-01 09:34:10 -04:00
|
|
|
has_many :events, as: :target, dependent: :destroy
|
|
|
|
|
2016-09-20 15:52:20 -04:00
|
|
|
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
|
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
|
|
|
|
2017-05-04 08:11:15 -04:00
|
|
|
belongs_to :assignee, class_name: "User"
|
|
|
|
|
2017-05-31 07:47:36 -04:00
|
|
|
serialize :merge_params, Hash # rubocop:disable Cop/ActiverecordSerialize
|
2015-11-02 11:27:38 -05:00
|
|
|
|
2016-08-02 08:38:03 -04:00
|
|
|
after_create :ensure_merge_request_diff, unless: :importing?
|
|
|
|
after_update :reload_diff_if_branch_changed
|
2014-01-22 08:20:20 -05:00
|
|
|
|
2016-12-01 06:17:30 -05:00
|
|
|
delegate :commits, :real_size, :commits_sha, :commits_count,
|
|
|
|
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
|
2016-08-11 03:00:17 -04:00
|
|
|
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
|
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|
|
2016-06-20 12:52:54 -04:00
|
|
|
Gitlab::Timeless.timeless(merge_request, &block)
|
2015-03-23 12:30:19 -04:00
|
|
|
end
|
2013-02-20 08:15:01 -05:00
|
|
|
end
|
2012-03-30 01:05:04 -04:00
|
|
|
|
2016-07-26 07:57:43 -04:00
|
|
|
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
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
|
2017-02-17 08:56:13 -05:00
|
|
|
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
|
2016-07-26 07:57:43 -04:00
|
|
|
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
|
|
|
validate :validate_fork, unless: :closed_without_fork?
|
2017-04-26 18:04:07 -04:00
|
|
|
validate :validate_target_project, on: :create
|
2011-11-28 02:39:43 -05:00
|
|
|
|
2016-12-09 05:12:38 -05:00
|
|
|
scope :by_source_or_target_branch, ->(branch_name) do
|
|
|
|
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
|
|
|
|
end
|
2013-02-26 03:38:40 -05:00
|
|
|
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
|
2013-08-08 10:29:31 -04:00
|
|
|
scope :of_projects, ->(ids) { where(target_project_id: ids) }
|
2016-03-09 09:25:48 -05:00
|
|
|
scope :from_project, ->(project) { where(source_project_id: project.id) }
|
2015-06-18 13:15:17 -04:00
|
|
|
scope :merged, -> { with_state(:merged) }
|
|
|
|
scope :closed_and_merged, -> { with_states(:closed, :merged) }
|
2016-07-28 00:04:57 -04:00
|
|
|
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
|
2013-02-21 07:11:24 -05:00
|
|
|
|
2015-11-11 06:50:36 -05:00
|
|
|
scope :join_project, -> { joins(:target_project) }
|
|
|
|
scope :references_project, -> { references(:target_project) }
|
2017-05-04 08:11:15 -04:00
|
|
|
scope :assigned, -> { where("assignee_id IS NOT NULL") }
|
|
|
|
scope :unassigned, -> { where("assignee_id IS NULL") }
|
|
|
|
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
|
|
|
|
|
|
|
|
participant :assignee
|
2015-11-11 06:50:36 -05:00
|
|
|
|
2016-07-03 19:58:58 -04:00
|
|
|
after_save :keep_around_commit
|
|
|
|
|
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
|
2016-03-24 11:41:48 -04:00
|
|
|
@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-11-30 15:14:46 -05:00
|
|
|
def self.link_reference_pattern
|
2016-03-24 11:41:48 -04:00
|
|
|
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
|
2015-11-30 15:14:46 -05:00
|
|
|
end
|
|
|
|
|
2016-06-18 13:55:45 -04:00
|
|
|
def self.reference_valid?(reference)
|
|
|
|
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
|
|
|
|
end
|
|
|
|
|
2016-10-19 09:49:07 -04:00
|
|
|
def self.project_foreign_key
|
|
|
|
'target_project_id'
|
|
|
|
end
|
|
|
|
|
2016-03-07 06:56:46 -05:00
|
|
|
# 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-03-22 06:57:20 -04:00
|
|
|
# unscoping unnecessary conditions that'll be applied
|
|
|
|
# when executing `where("merge_requests.id IN (#{union.to_sql})")`
|
|
|
|
source = unscoped.where(source_project_id: relation).select(:id)
|
|
|
|
target = unscoped.where(target_project_id: relation).select(:id)
|
2016-03-07 06:56:46 -05:00
|
|
|
union = Gitlab::SQL::Union.new([source, target])
|
|
|
|
|
|
|
|
where("merge_requests.id IN (#{union.to_sql})")
|
|
|
|
end
|
|
|
|
|
2016-09-08 05:18:41 -04:00
|
|
|
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
|
|
|
|
|
|
|
|
def self.work_in_progress?(title)
|
|
|
|
!!(title =~ WIP_REGEX)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.wipless_title(title)
|
|
|
|
title.sub(WIP_REGEX, "")
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.wip_title(title)
|
|
|
|
work_in_progress?(title) ? title : "WIP: #{title}"
|
|
|
|
end
|
|
|
|
|
2017-05-04 08:11:15 -04:00
|
|
|
# Returns a Hash of attributes to be used for Twitter card metadata
|
|
|
|
def card_attributes
|
|
|
|
{
|
|
|
|
'Author' => author.try(:name),
|
|
|
|
'Assignee' => assignee.try(:name)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
# This method is needed for compatibility with issues to not mess view and other code
|
|
|
|
def assignees
|
|
|
|
Array(assignee)
|
|
|
|
end
|
|
|
|
|
|
|
|
def assignee_or_author?(user)
|
|
|
|
author_id == user.id || assignee_id == user.id
|
|
|
|
end
|
|
|
|
|
2017-01-11 07:44:12 -05:00
|
|
|
# `from` argument can be a Namespace or Project.
|
2017-01-11 07:45:49 -05:00
|
|
|
def to_reference(from = nil, full: false)
|
2015-05-02 23:11:21 -04:00
|
|
|
reference = "#{self.class.reference_prefix}#{iid}"
|
|
|
|
|
2017-01-11 07:45:49 -05:00
|
|
|
"#{project.to_reference(from, full: full)}#{reference}"
|
2015-05-02 23:11:21 -04:00
|
|
|
end
|
|
|
|
|
2015-10-20 08:23:56 -04:00
|
|
|
def first_commit
|
|
|
|
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
|
2015-10-23 11:01:10 -04:00
|
|
|
end
|
2015-10-20 08:23:56 -04:00
|
|
|
|
2016-07-27 13:00:34 -04:00
|
|
|
def raw_diffs(*args)
|
2016-08-03 17:32:12 -04:00
|
|
|
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
|
2016-06-28 12:25:32 -04:00
|
|
|
end
|
|
|
|
|
2017-04-20 10:47:32 -04:00
|
|
|
def diffs(diff_options = {})
|
2016-08-15 10:57:19 -04:00
|
|
|
if compare
|
2017-05-26 19:27:30 -04:00
|
|
|
# When saving MR diffs, `expanded` is implicitly added (because we need
|
2017-04-20 10:47:32 -04:00
|
|
|
# to save the entire contents to the DB), so add that here for
|
|
|
|
# consistency.
|
2017-05-26 19:27:30 -04:00
|
|
|
compare.diffs(diff_options.merge(expanded: true))
|
2016-07-27 07:09:52 -04:00
|
|
|
else
|
2016-08-15 10:57:19 -04:00
|
|
|
merge_request_diff.diffs(diff_options)
|
2016-07-27 07:09:52 -04:00
|
|
|
end
|
2016-06-28 12:25:32 -04:00
|
|
|
end
|
|
|
|
|
2016-03-03 12:38:44 -05:00
|
|
|
def diff_size
|
2017-04-20 10:47:32 -04:00
|
|
|
# Calling `merge_request_diff.diffs.real_size` will also perform
|
|
|
|
# highlighting, which we don't need here.
|
|
|
|
return real_size if merge_request_diff
|
2016-12-26 09:27:20 -05:00
|
|
|
|
2017-04-20 10:47:32 -04:00
|
|
|
diffs.real_size
|
2016-03-03 12:38:44 -05:00
|
|
|
end
|
|
|
|
|
2016-01-20 12:44:27 -05:00
|
|
|
def diff_base_commit
|
2016-06-20 12:48:04 -04:00
|
|
|
if persisted?
|
2016-01-20 12:44:27 -05:00
|
|
|
merge_request_diff.base_commit
|
2016-08-08 16:00:11 -04:00
|
|
|
else
|
|
|
|
branch_merge_base_commit
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def diff_start_commit
|
|
|
|
if persisted?
|
|
|
|
merge_request_diff.start_commit
|
|
|
|
else
|
|
|
|
target_branch_head
|
2016-01-20 12:44:27 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
def diff_head_commit
|
|
|
|
if persisted?
|
|
|
|
merge_request_diff.head_commit
|
|
|
|
else
|
|
|
|
source_branch_head
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def diff_start_sha
|
|
|
|
diff_start_commit.try(:sha)
|
|
|
|
end
|
|
|
|
|
|
|
|
def diff_base_sha
|
|
|
|
diff_base_commit.try(:sha)
|
|
|
|
end
|
|
|
|
|
|
|
|
def diff_head_sha
|
|
|
|
diff_head_commit.try(:sha)
|
|
|
|
end
|
|
|
|
|
|
|
|
# When importing a pull request from GitHub, the old and new branches may no
|
|
|
|
# longer actually exist by those names, but we need to recreate the merge
|
|
|
|
# request diff with the right source and target shas.
|
|
|
|
# We use these attributes to force these to the intended values.
|
|
|
|
attr_writer :target_branch_sha, :source_branch_sha
|
|
|
|
|
|
|
|
def source_branch_head
|
2017-05-18 16:48:42 -04:00
|
|
|
return unless source_project
|
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
source_branch_ref = @source_branch_sha || source_branch
|
2016-08-25 08:46:59 -04:00
|
|
|
source_project.repository.commit(source_branch_ref) if source_branch_ref
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def target_branch_head
|
|
|
|
target_branch_ref = @target_branch_sha || target_branch
|
2016-08-25 08:46:59 -04:00
|
|
|
target_project.repository.commit(target_branch_ref) if target_branch_ref
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
|
2016-08-08 16:00:11 -04:00
|
|
|
def branch_merge_base_commit
|
|
|
|
start_sha = target_branch_sha
|
|
|
|
head_sha = source_branch_sha
|
|
|
|
|
|
|
|
if start_sha && head_sha
|
|
|
|
target_project.merge_base_commit(start_sha, head_sha)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
def target_branch_sha
|
2016-07-31 18:44:02 -04:00
|
|
|
@target_branch_sha || target_branch_head.try(:sha)
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def source_branch_sha
|
2016-07-31 18:44:02 -04:00
|
|
|
@source_branch_sha || source_branch_head.try(:sha)
|
2015-10-20 08:23:56 -04:00
|
|
|
end
|
|
|
|
|
2016-08-22 07:18:58 -04:00
|
|
|
def diff_refs
|
2017-05-15 14:19:49 -04:00
|
|
|
if persisted?
|
2016-07-28 16:40:13 -04:00
|
|
|
merge_request_diff.diff_refs
|
2016-08-08 16:00:11 -04:00
|
|
|
else
|
2017-05-15 14:19:49 -04:00
|
|
|
Gitlab::Diff::DiffRefs.new(
|
|
|
|
base_sha: diff_base_sha,
|
|
|
|
start_sha: diff_start_sha,
|
|
|
|
head_sha: diff_head_sha
|
|
|
|
)
|
2016-07-28 16:40:13 -04:00
|
|
|
end
|
2016-06-20 12:51:48 -04:00
|
|
|
end
|
|
|
|
|
2016-08-22 07:18:58 -04:00
|
|
|
def branch_merge_base_sha
|
|
|
|
branch_merge_base_commit.try(:sha)
|
|
|
|
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?
|
2016-04-12 14:39:33 -04:00
|
|
|
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(: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,
|
2015-12-14 21:53:52 -05:00
|
|
|
"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
|
|
|
|
|
2017-04-26 18:04:07 -04:00
|
|
|
def validate_target_project
|
|
|
|
return true if target_project.merge_requests_enabled?
|
|
|
|
|
|
|
|
errors.add :base, 'Target project has disabled merge requests'
|
|
|
|
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
|
2016-07-26 07:57:43 -04:00
|
|
|
return true if target_project == source_project
|
2016-10-19 14:33:51 -04:00
|
|
|
return true unless source_project_missing?
|
2014-04-03 03:36:10 -04:00
|
|
|
|
2016-07-26 07:57:43 -04:00
|
|
|
errors.add :validate_fork,
|
2016-08-25 09:08:31 -04:00
|
|
|
'Source project is not a fork of the target project'
|
2016-07-26 07:57:43 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def closed_without_fork?
|
2016-10-19 14:33:51 -04:00
|
|
|
closed? && source_project_missing?
|
2016-07-26 07:57:43 -04:00
|
|
|
end
|
|
|
|
|
2016-10-19 14:33:51 -04:00
|
|
|
def source_project_missing?
|
2016-07-26 07:57:43 -04:00
|
|
|
return false unless for_fork?
|
|
|
|
return true unless source_project
|
|
|
|
|
|
|
|
!source_project.forked_from?(target_project)
|
2014-04-02 14:51:53 -04:00
|
|
|
end
|
|
|
|
|
2016-09-13 08:40:00 -04:00
|
|
|
def reopenable?
|
2016-10-19 12:13:04 -04:00
|
|
|
closed? && !source_project_missing? && source_branch_exists?
|
2016-09-06 10:48:59 -04:00
|
|
|
end
|
|
|
|
|
2016-08-02 08:38:03 -04:00
|
|
|
def ensure_merge_request_diff
|
|
|
|
merge_request_diff || create_merge_request_diff
|
2016-07-25 10:04:55 -04:00
|
|
|
end
|
|
|
|
|
2016-08-02 09:28:37 -04:00
|
|
|
def create_merge_request_diff
|
|
|
|
merge_request_diffs.create
|
|
|
|
reload_merge_request_diff
|
|
|
|
end
|
|
|
|
|
|
|
|
def reload_merge_request_diff
|
|
|
|
merge_request_diff(true)
|
|
|
|
end
|
|
|
|
|
2017-04-30 16:32:09 -04:00
|
|
|
def merge_request_diff_for(diff_refs_or_sha)
|
|
|
|
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
|
|
|
|
diffs = merge_request_diffs.viewable.select_without_diff
|
|
|
|
h[diff_refs_or_sha] =
|
|
|
|
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
|
|
|
|
diffs.find_by_diff_refs(diff_refs_or_sha)
|
|
|
|
else
|
|
|
|
diffs.find_by(head_commit_sha: diff_refs_or_sha)
|
|
|
|
end
|
2017-03-31 19:49:43 -04:00
|
|
|
end
|
|
|
|
|
2017-04-30 16:32:09 -04:00
|
|
|
@merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
|
2017-03-31 19:39:14 -04:00
|
|
|
end
|
|
|
|
|
2017-05-21 16:38:33 -04:00
|
|
|
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
|
|
|
|
|
2016-08-02 08:38:03 -04:00
|
|
|
def reload_diff_if_branch_changed
|
2014-02-18 13:17:26 -05:00
|
|
|
if source_branch_changed? || target_branch_changed?
|
2016-06-20 13:22:39 -04:00
|
|
|
reload_diff
|
2014-02-18 13:17:26 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-05-21 16:38:33 -04:00
|
|
|
def reload_diff(current_user = nil)
|
2016-07-25 07:10:21 -04:00
|
|
|
return unless open?
|
|
|
|
|
2016-06-20 13:22:39 -04:00
|
|
|
old_diff_refs = self.diff_refs
|
2016-07-25 11:42:57 -04:00
|
|
|
create_merge_request_diff
|
2016-07-20 12:25:36 -04:00
|
|
|
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
|
2016-06-20 13:22:39 -04:00
|
|
|
new_diff_refs = self.diff_refs
|
|
|
|
|
2017-05-31 15:31:33 -04:00
|
|
|
update_diff_discussion_positions(
|
2016-06-20 13:22:39 -04:00
|
|
|
old_diff_refs: old_diff_refs,
|
2017-05-21 16:38:33 -04:00
|
|
|
new_diff_refs: new_diff_refs,
|
|
|
|
current_user: current_user
|
2016-06-20 13:22:39 -04:00
|
|
|
)
|
2012-03-15 19:45:46 -04:00
|
|
|
end
|
|
|
|
|
2012-03-30 01:05:04 -04:00
|
|
|
def check_if_can_be_merged
|
2016-01-05 10:24:42 -05:00
|
|
|
return unless unchecked?
|
|
|
|
|
2015-08-11 08:33:31 -04:00
|
|
|
can_be_merged =
|
2016-06-20 12:48:04 -04:00
|
|
|
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
|
2015-08-11 08:33:31 -04:00
|
|
|
|
|
|
|
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
|
2016-07-08 06:35:31 -04:00
|
|
|
@merge_event ||= 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
|
2016-07-08 06:35:31 -04:00
|
|
|
@closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
|
2012-03-15 19:45:46 -04:00
|
|
|
end
|
|
|
|
|
2015-04-30 09:43:32 -04:00
|
|
|
def work_in_progress?
|
2016-09-08 05:18:41 -04:00
|
|
|
self.class.work_in_progress?(title)
|
2016-02-26 22:34:14 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def wipless_title
|
2016-09-08 05:18:41 -04:00
|
|
|
self.class.wipless_title(self.title)
|
|
|
|
end
|
|
|
|
|
|
|
|
def wip_title
|
|
|
|
self.class.wip_title(self.title)
|
2015-04-30 09:43:32 -04:00
|
|
|
end
|
|
|
|
|
2016-06-24 12:13:56 -04:00
|
|
|
def mergeable?(skip_ci_check: false)
|
|
|
|
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
|
2016-01-05 10:24:42 -05:00
|
|
|
|
|
|
|
check_if_can_be_merged
|
|
|
|
|
|
|
|
can_be_merged?
|
2015-04-30 09:43:32 -04:00
|
|
|
end
|
|
|
|
|
2016-06-24 12:13:56 -04:00
|
|
|
def mergeable_state?(skip_ci_check: false)
|
2016-06-07 07:01:34 -04:00
|
|
|
return false unless open?
|
|
|
|
return false if work_in_progress?
|
|
|
|
return false if broken?
|
2016-06-24 12:13:56 -04:00
|
|
|
return false unless skip_ci_check || mergeable_ci_state?
|
2016-09-16 07:02:42 -04:00
|
|
|
return false unless mergeable_discussions_state?
|
2016-06-07 07:01:34 -04:00
|
|
|
|
|
|
|
true
|
2015-04-30 09:43:32 -04:00
|
|
|
end
|
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
def can_cancel_merge_when_pipeline_succeeds?(current_user)
|
2015-11-24 08:59:02 -05:00
|
|
|
can_be_merged_by?(current_user) || self.author == current_user
|
2015-11-23 03:42:20 -05:00
|
|
|
end
|
|
|
|
|
2015-11-24 08:59:02 -05:00
|
|
|
def can_remove_source_branch?(current_user)
|
2017-04-03 13:59:58 -04:00
|
|
|
!ProtectedBranch.protected?(source_project, source_branch) &&
|
2015-11-24 08:59:02 -05:00
|
|
|
!source_project.root_ref?(source_branch) &&
|
2016-08-08 14:55:13 -04:00
|
|
|
Ability.allowed?(current_user, :push_code, source_project) &&
|
2016-06-20 12:48:04 -04:00
|
|
|
diff_head_commit == source_branch_head
|
2015-11-18 05:17:41 -05:00
|
|
|
end
|
|
|
|
|
2016-02-12 15:41:31 -05:00
|
|
|
def should_remove_source_branch?
|
2016-11-03 10:15:03 -04:00
|
|
|
Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
|
2016-02-12 15:41:31 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def force_remove_source_branch?
|
2016-11-03 10:15:03 -04:00
|
|
|
Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
|
2016-02-12 15:41:31 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def remove_source_branch?
|
|
|
|
should_remove_source_branch? || force_remove_source_branch?
|
|
|
|
end
|
|
|
|
|
2016-12-08 20:56:31 -05:00
|
|
|
def related_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" +
|
2015-10-22 12:38:00 -04:00
|
|
|
"((project_id = :source_project_id OR project_id = :target_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
|
|
|
|
2017-03-09 20:29:11 -05:00
|
|
|
alias_method :discussion_notes, :related_notes
|
2016-08-17 13:14:44 -04:00
|
|
|
|
2016-09-16 07:02:42 -04:00
|
|
|
def mergeable_discussions_state?
|
|
|
|
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
|
|
|
|
|
2016-11-23 07:00:58 -05:00
|
|
|
!discussions_to_be_resolved?
|
2016-09-16 07:02:42 -04:00
|
|
|
end
|
|
|
|
|
2014-09-15 03:10:35 -04:00
|
|
|
def hook_attrs
|
|
|
|
attrs = {
|
2016-04-12 14:39:33 -04:00
|
|
|
source: source_project.try(:hook_attrs),
|
2014-09-15 03:10:35 -04:00
|
|
|
target: target_project.hook_attrs,
|
2015-09-25 16:07:08 -04:00
|
|
|
last_commit: nil,
|
2017-03-14 12:56:15 -04:00
|
|
|
work_in_progress: work_in_progress?,
|
|
|
|
total_time_spent: total_time_spent,
|
|
|
|
human_total_time_spent: human_total_time_spent,
|
|
|
|
human_time_estimate: human_time_estimate
|
2014-09-15 03:10:35 -04:00
|
|
|
}
|
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
if diff_head_commit
|
2017-02-21 18:50:22 -05:00
|
|
|
attrs[:last_commit] = diff_head_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
|
|
|
|
|
2017-03-21 11:25:00 -04:00
|
|
|
def project
|
|
|
|
target_project
|
|
|
|
end
|
|
|
|
|
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
|
|
|
# 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-09-30 10:53:44 -04:00
|
|
|
# This optimization does not apply to issues from external sources.
|
2017-01-31 09:42:54 -05:00
|
|
|
def cache_merge_request_closes_issues!(current_user)
|
2016-09-30 10:53:44 -04:00
|
|
|
return if project.has_external_issue_tracker?
|
|
|
|
|
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
|
|
|
transaction do
|
2016-09-20 05:36:54 -04:00
|
|
|
self.merge_requests_closing_issues.delete_all
|
2016-09-30 10:53:44 -04:00
|
|
|
|
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
|
|
|
closes_issues(current_user).each do |issue|
|
2016-09-20 08:43:11 -04:00
|
|
|
self.merge_requests_closing_issues.create!(issue: issue)
|
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
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
|
2017-02-13 10:04:06 -05:00
|
|
|
messages = [title, description]
|
2016-09-20 15:17:37 -04:00
|
|
|
messages.concat(commits.map(&:safe_message)) if merge_request_diff
|
2016-01-27 08:09:58 -05:00
|
|
|
|
2017-06-21 09:48:12 -04:00
|
|
|
Gitlab::ClosingIssueExtractor.new(project, current_user)
|
|
|
|
.closed_by_message(messages.join("\n"))
|
2013-05-30 19:16:49 -04:00
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-01-31 09:42:54 -05:00
|
|
|
def issues_mentioned_but_not_closing(current_user)
|
2016-12-05 06:44:29 -05:00
|
|
|
return [] unless target_branch == project.default_branch
|
2016-11-23 16:50:49 -05:00
|
|
|
|
2016-12-05 06:44:29 -05:00
|
|
|
ext = Gitlab::ReferenceExtractor.new(project, current_user)
|
2017-02-13 10:04:06 -05:00
|
|
|
ext.analyze("#{title}\n#{description}")
|
2016-11-23 16:50:49 -05:00
|
|
|
|
2017-01-18 04:05:09 -05:00
|
|
|
ext.issues - closes_issues(current_user)
|
2016-11-23 16:50:49 -05:00
|
|
|
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
|
2017-02-14 09:03:24 -05:00
|
|
|
source_project.namespace.full_path
|
2014-02-15 07:36:58 -05:00
|
|
|
else
|
|
|
|
"(removed)"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-05-08 08:37:36 -04:00
|
|
|
def target_project_namespace
|
|
|
|
if target_project && target_project.namespace
|
2017-02-14 09:03:24 -05:00
|
|
|
target_project.namespace.full_path
|
2014-05-08 08:37:36 -04:00
|
|
|
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
|
|
|
|
|
2016-11-28 04:48:55 -05:00
|
|
|
def merge_commit_message(include_description: false)
|
2016-11-24 16:01:05 -05:00
|
|
|
closes_issues_references = closes_issues.map do |issue|
|
|
|
|
issue.to_reference(target_project)
|
|
|
|
end
|
|
|
|
|
2016-12-05 06:29:17 -05:00
|
|
|
message = [
|
|
|
|
"Merge branch '#{source_branch}' into '#{target_branch}'",
|
|
|
|
title
|
|
|
|
]
|
2016-11-24 16:01:05 -05:00
|
|
|
|
2016-12-05 06:29:17 -05:00
|
|
|
if !include_description && closes_issues_references.present?
|
2016-12-13 08:00:00 -05:00
|
|
|
message << "Closes #{closes_issues_references.to_sentence}"
|
2016-11-24 16:01:05 -05:00
|
|
|
end
|
|
|
|
|
2016-12-05 06:29:17 -05:00
|
|
|
message << "#{description}" if include_description && description.present?
|
2016-09-21 10:22:54 -04:00
|
|
|
message << "See merge request #{to_reference}"
|
|
|
|
|
2016-12-05 06:29:17 -05:00
|
|
|
message.join("\n\n")
|
2014-01-13 10:20:30 -05:00
|
|
|
end
|
2014-01-31 06:24:38 -05:00
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
def reset_merge_when_pipeline_succeeds
|
|
|
|
return unless merge_when_pipeline_succeeds?
|
2015-11-18 05:17:41 -05:00
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
self.merge_when_pipeline_succeeds = false
|
2015-11-02 11:27:38 -05:00
|
|
|
self.merge_user = nil
|
2016-02-12 15:41:31 -05:00
|
|
|
if merge_params
|
|
|
|
merge_params.delete('should_remove_source_branch')
|
|
|
|
merge_params.delete('commit_message')
|
|
|
|
end
|
2015-11-02 11:27:38 -05:00
|
|
|
|
|
|
|
self.save
|
|
|
|
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?
|
2017-02-14 17:08:30 -05:00
|
|
|
has_ci_integration = source_project.try(:ci_service)
|
|
|
|
uses_gitlab_ci = all_pipelines.any?
|
|
|
|
|
|
|
|
(has_ci_integration || uses_gitlab_ci) && commits.any?
|
2015-06-11 03:40:26 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def branch_missing?
|
|
|
|
!source_branch_exists? || !target_branch_exists?
|
|
|
|
end
|
2015-06-11 12:45:12 -04:00
|
|
|
|
2016-01-05 10:24:42 -05:00
|
|
|
def broken?
|
2016-12-01 06:17:30 -05:00
|
|
|
has_no_commits? || branch_missing? || cannot_be_merged?
|
2016-01-05 10:24:42 -05:00
|
|
|
end
|
|
|
|
|
2015-06-11 12:45:12 -04:00
|
|
|
def can_be_merged_by?(user)
|
2016-07-18 04:16:56 -04:00
|
|
|
access = ::Gitlab::UserAccess.new(user, project: project)
|
|
|
|
access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch)
|
|
|
|
end
|
|
|
|
|
|
|
|
def can_be_merged_via_command_line_by?(user)
|
|
|
|
access = ::Gitlab::UserAccess.new(user, project: project)
|
|
|
|
access.can_push_to_branch?(target_branch)
|
2016-07-04 01:36:32 -04:00
|
|
|
end
|
|
|
|
|
2016-06-09 10:08:38 -04:00
|
|
|
def mergeable_ci_state?
|
2017-02-17 08:56:13 -05:00
|
|
|
return true unless project.only_allow_merge_if_pipeline_succeeds?
|
2016-06-07 07:01:34 -04:00
|
|
|
|
2016-11-28 05:13:32 -05:00
|
|
|
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
|
2016-04-27 14:34:42 -04:00
|
|
|
end
|
|
|
|
|
2017-02-06 19:06:46 -05:00
|
|
|
def environments_for(current_user)
|
2016-09-16 04:16:21 -04:00
|
|
|
return [] unless diff_head_commit
|
2016-08-09 09:11:14 -04:00
|
|
|
|
2017-02-06 19:06:46 -05:00
|
|
|
@environments ||= Hash.new do |h, current_user|
|
|
|
|
envs = EnvironmentsFinder.new(target_project, current_user,
|
|
|
|
ref: target_branch, commit: diff_head_commit, with_tags: true).execute
|
2016-11-09 08:46:36 -05:00
|
|
|
|
2017-02-06 19:06:46 -05:00
|
|
|
if source_project
|
|
|
|
envs.concat EnvironmentsFinder.new(source_project, current_user,
|
|
|
|
ref: source_branch, commit: diff_head_commit).execute
|
|
|
|
end
|
2016-11-09 08:46:36 -05:00
|
|
|
|
2017-02-06 19:06:46 -05:00
|
|
|
h[current_user] = envs.uniq
|
2016-11-09 08:46:36 -05:00
|
|
|
end
|
2017-02-06 19:06:46 -05:00
|
|
|
|
|
|
|
@environments[current_user]
|
2016-08-02 08:01:22 -04:00
|
|
|
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
|
|
|
|
2016-02-29 15:13:00 -05:00
|
|
|
def state_icon_name
|
|
|
|
if merged?
|
|
|
|
"check"
|
|
|
|
elsif closed?
|
|
|
|
"times"
|
|
|
|
else
|
|
|
|
"circle-o"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-08-11 08:33:31 -04:00
|
|
|
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
|
|
|
|
|
2016-07-03 19:58:58 -04:00
|
|
|
def ref_fetched?
|
|
|
|
project.repository.ref_exists?(ref_path)
|
2015-09-21 10:25:59 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def ensure_ref_fetched
|
2016-07-03 19:58:58 -04:00
|
|
|
fetch_ref unless ref_fetched?
|
2015-09-21 10:25:59 -04:00
|
|
|
end
|
|
|
|
|
2015-08-11 08:33:31 -04:00
|
|
|
def in_locked_state
|
|
|
|
begin
|
|
|
|
lock_mr
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
unlock_mr if locked?
|
|
|
|
end
|
|
|
|
end
|
2015-10-23 11:01:10 -04:00
|
|
|
|
2016-02-04 13:23:58 -05:00
|
|
|
def diverged_commits_count
|
|
|
|
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
|
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
|
2016-02-04 13:23:58 -05:00
|
|
|
cache = {
|
2016-06-20 12:48:04 -04:00
|
|
|
source_sha: source_branch_sha,
|
|
|
|
target_sha: target_branch_sha,
|
2016-02-04 13:23:58 -05:00
|
|
|
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-06-20 12:48:04 -04:00
|
|
|
return 0 unless source_branch_sha && target_branch_sha
|
2016-03-18 13:16:04 -04:00
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
|
2016-02-04 13:23:58 -05:00
|
|
|
end
|
2016-03-18 13:16:04 -04:00
|
|
|
private :compute_diverged_commits_count
|
2016-02-04 13:23:58 -05:00
|
|
|
|
|
|
|
def diverged_from_target_branch?
|
|
|
|
diverged_commits_count > 0
|
|
|
|
end
|
|
|
|
|
2016-08-12 10:22:48 -04:00
|
|
|
def all_pipelines
|
2016-11-28 06:03:53 -05:00
|
|
|
return Ci::Pipeline.none unless source_project
|
2016-09-19 11:51:27 -04:00
|
|
|
|
2016-10-17 08:15:06 -04:00
|
|
|
@all_pipelines ||= source_project.pipelines
|
2016-10-17 09:31:21 -04:00
|
|
|
.where(sha: all_commits_sha, ref: source_branch)
|
|
|
|
.order(id: :desc)
|
2016-09-20 05:36:21 -04:00
|
|
|
end
|
2016-09-19 11:51:27 -04:00
|
|
|
|
2016-09-20 07:18:48 -04:00
|
|
|
# Note that this could also return SHA from now dangling commits
|
2016-10-17 08:15:06 -04:00
|
|
|
#
|
2016-09-20 05:36:21 -04:00
|
|
|
def all_commits_sha
|
2016-10-17 08:15:06 -04:00
|
|
|
if persisted?
|
|
|
|
merge_request_diffs.flat_map(&:commits_sha).uniq
|
2016-10-18 06:05:47 -04:00
|
|
|
elsif compare_commits
|
|
|
|
compare_commits.to_a.reverse.map(&:id)
|
2016-10-17 08:15:06 -04:00
|
|
|
else
|
2016-10-18 06:05:47 -04:00
|
|
|
[diff_head_sha]
|
2016-10-17 08:15:06 -04:00
|
|
|
end
|
2016-08-12 10:22:48 -04:00
|
|
|
end
|
|
|
|
|
2016-02-14 22:36:30 -05:00
|
|
|
def merge_commit
|
|
|
|
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
|
|
|
|
end
|
|
|
|
|
2016-11-29 08:47:43 -05:00
|
|
|
def can_be_reverted?(current_user)
|
2016-02-18 11:58:04 -05:00
|
|
|
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
|
2016-02-14 22:36:30 -05:00
|
|
|
end
|
2016-04-20 07:30:13 -04:00
|
|
|
|
|
|
|
def can_be_cherry_picked?
|
2017-05-09 00:15:34 -04:00
|
|
|
merge_commit.present?
|
2016-04-20 07:30:13 -04:00
|
|
|
end
|
2016-07-03 19:58:58 -04:00
|
|
|
|
2016-08-04 05:31:44 -04:00
|
|
|
def has_complete_diff_refs?
|
2017-05-15 14:19:49 -04:00
|
|
|
diff_refs && diff_refs.complete?
|
2016-06-20 13:22:08 -04:00
|
|
|
end
|
|
|
|
|
2017-05-31 15:31:33 -04:00
|
|
|
def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
|
2016-08-04 05:31:44 -04:00
|
|
|
return unless has_complete_diff_refs?
|
2016-06-20 13:22:39 -04:00
|
|
|
return if new_diff_refs == old_diff_refs
|
|
|
|
|
2017-05-31 15:31:33 -04:00
|
|
|
active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
|
|
|
|
discussion.active?(old_diff_refs)
|
2016-06-20 13:22:39 -04:00
|
|
|
end
|
2017-05-31 15:31:33 -04:00
|
|
|
return if active_diff_discussions.empty?
|
2016-06-20 13:22:39 -04:00
|
|
|
|
2017-05-31 15:31:33 -04:00
|
|
|
paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
|
2016-06-20 13:22:39 -04:00
|
|
|
|
2017-05-31 15:31:33 -04:00
|
|
|
service = Discussions::UpdateDiffPositionService.new(
|
2016-06-20 13:22:39 -04:00
|
|
|
self.project,
|
2017-05-21 16:38:33 -04:00
|
|
|
current_user,
|
2016-06-20 13:22:39 -04:00
|
|
|
old_diff_refs: old_diff_refs,
|
|
|
|
new_diff_refs: new_diff_refs,
|
|
|
|
paths: paths
|
|
|
|
)
|
|
|
|
|
2017-05-31 15:31:33 -04:00
|
|
|
active_diff_discussions.each do |discussion|
|
|
|
|
service.execute(discussion)
|
2016-06-20 13:22:39 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-03 19:58:58 -04:00
|
|
|
def keep_around_commit
|
|
|
|
project.repository.keep_around(self.merge_commit_sha)
|
|
|
|
end
|
2016-08-04 05:31:44 -04:00
|
|
|
|
2016-12-01 06:17:30 -05:00
|
|
|
def has_commits?
|
2016-11-24 09:05:15 -05:00
|
|
|
merge_request_diff && commits_count > 0
|
2016-12-01 06:17:30 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def has_no_commits?
|
|
|
|
!has_commits?
|
|
|
|
end
|
2016-11-24 09:05:15 -05:00
|
|
|
|
2017-05-31 01:50:53 -04:00
|
|
|
def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
|
2016-11-24 09:05:15 -05:00
|
|
|
return false unless can_be_merged_by?(current_user)
|
|
|
|
|
|
|
|
return true if autocomplete_precheck
|
|
|
|
|
|
|
|
return false unless mergeable?(skip_ci_check: true)
|
|
|
|
return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
|
|
|
|
return false if last_diff_sha != diff_head_sha
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
2011-11-28 02:39:43 -05:00
|
|
|
end
|