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
|
2016-06-01 16:03:51 +00:00
|
|
|
include Importable
|
2012-06-07 12:44:57 +00:00
|
|
|
|
2016-10-18 21:20:36 +00:00
|
|
|
belongs_to :target_project, class_name: "Project"
|
|
|
|
belongs_to :source_project, class_name: "Project"
|
2015-11-02 16:27:38 +00:00
|
|
|
belongs_to :merge_user, class_name: "User"
|
2014-01-22 18:22:20 +00:00
|
|
|
|
2016-07-25 11:10:21 +00:00
|
|
|
has_many :merge_request_diffs, dependent: :destroy
|
2016-08-02 13:28:37 +00:00
|
|
|
has_one :merge_request_diff,
|
|
|
|
-> { order('merge_request_diffs.id DESC') }
|
|
|
|
|
2016-07-01 13:34:10 +00:00
|
|
|
has_many :events, as: :target, dependent: :destroy
|
|
|
|
|
2016-09-20 19:52:20 +00: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 08:59:36 +00:00
|
|
|
|
2015-11-02 16:27:38 +00:00
|
|
|
serialize :merge_params, Hash
|
|
|
|
|
2016-08-02 12:38:03 +00:00
|
|
|
after_create :ensure_merge_request_diff, unless: :importing?
|
|
|
|
after_update :reload_diff_if_branch_changed
|
2014-01-22 13:20:20 +00:00
|
|
|
|
2016-12-01 11:17:30 +00:00
|
|
|
delegate :commits, :real_size, :commits_sha, :commits_count,
|
|
|
|
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
|
2016-08-11 07:00:17 +00:00
|
|
|
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
|
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|
|
2016-06-20 16:52:54 +00:00
|
|
|
Gitlab::Timeless.timeless(merge_request, &block)
|
2015-03-23 16:30:19 +00:00
|
|
|
end
|
2013-02-20 13:15:01 +00:00
|
|
|
end
|
2012-03-30 05:05:04 +00:00
|
|
|
|
2016-07-26 11:57:43 +00:00
|
|
|
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
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
|
2017-02-17 13:56:13 +00:00
|
|
|
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
|
2016-07-26 11:57:43 +00:00
|
|
|
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
|
|
|
validate :validate_fork, unless: :closed_without_fork?
|
2011-11-28 07:39:43 +00:00
|
|
|
|
2016-12-09 10:12:38 +00:00
|
|
|
scope :by_source_or_target_branch, ->(branch_name) do
|
|
|
|
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
|
|
|
|
end
|
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-08-08 14:29:31 +00:00
|
|
|
scope :of_projects, ->(ids) { where(target_project_id: ids) }
|
2016-03-09 14:25:48 +00:00
|
|
|
scope :from_project, ->(project) { where(source_project_id: project.id) }
|
2015-06-18 17:15:17 +00:00
|
|
|
scope :merged, -> { with_state(:merged) }
|
|
|
|
scope :closed_and_merged, -> { with_states(:closed, :merged) }
|
2016-07-28 04:04:57 +00:00
|
|
|
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
|
2013-02-21 12:11:24 +00:00
|
|
|
|
2015-11-11 11:50:36 +00:00
|
|
|
scope :join_project, -> { joins(:target_project) }
|
|
|
|
scope :references_project, -> { references(:target_project) }
|
|
|
|
|
2016-07-03 23:58:58 +00:00
|
|
|
after_save :keep_around_commit
|
|
|
|
|
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
|
2016-03-24 15:41:48 +00:00
|
|
|
@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-11-30 20:14:46 +00:00
|
|
|
def self.link_reference_pattern
|
2016-03-24 15:41:48 +00:00
|
|
|
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
|
2015-11-30 20:14:46 +00:00
|
|
|
end
|
|
|
|
|
2016-06-18 17:55:45 +00:00
|
|
|
def self.reference_valid?(reference)
|
|
|
|
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
|
|
|
|
end
|
|
|
|
|
2016-10-19 13:49:07 +00:00
|
|
|
def self.project_foreign_key
|
|
|
|
'target_project_id'
|
|
|
|
end
|
|
|
|
|
2016-03-07 11:56:46 +00: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 10:57:20 +00: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 11:56:46 +00:00
|
|
|
union = Gitlab::SQL::Union.new([source, target])
|
|
|
|
|
|
|
|
where("merge_requests.id IN (#{union.to_sql})")
|
|
|
|
end
|
|
|
|
|
2016-09-08 09:18:41 +00: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-01-11 12:44:12 +00:00
|
|
|
# `from` argument can be a Namespace or Project.
|
2017-01-11 12:45:49 +00:00
|
|
|
def to_reference(from = nil, full: false)
|
2015-05-03 03:11:21 +00:00
|
|
|
reference = "#{self.class.reference_prefix}#{iid}"
|
|
|
|
|
2017-01-11 12:45:49 +00:00
|
|
|
"#{project.to_reference(from, full: full)}#{reference}"
|
2015-05-03 03:11:21 +00:00
|
|
|
end
|
|
|
|
|
2015-10-20 12:23:56 +00:00
|
|
|
def first_commit
|
|
|
|
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
|
2015-10-23 15:01:10 +00:00
|
|
|
end
|
2015-10-20 12:23:56 +00:00
|
|
|
|
2016-07-27 17:00:34 +00:00
|
|
|
def raw_diffs(*args)
|
2016-08-03 21:32:12 +00:00
|
|
|
merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
|
2016-06-28 16:25:32 +00:00
|
|
|
end
|
|
|
|
|
2016-07-27 17:00:34 +00:00
|
|
|
def diffs(diff_options = nil)
|
2016-08-15 14:57:19 +00:00
|
|
|
if compare
|
|
|
|
compare.diffs(diff_options)
|
2016-07-27 11:09:52 +00:00
|
|
|
else
|
2016-08-15 14:57:19 +00:00
|
|
|
merge_request_diff.diffs(diff_options)
|
2016-07-27 11:09:52 +00:00
|
|
|
end
|
2016-06-28 16:25:32 +00:00
|
|
|
end
|
|
|
|
|
2016-03-03 17:38:44 +00:00
|
|
|
def diff_size
|
2017-02-09 12:29:56 +00:00
|
|
|
# The `#diffs` method ends up at an instance of a class inheriting from
|
|
|
|
# `Gitlab::Diff::FileCollection::Base`, so use those options as defaults
|
|
|
|
# here too, to get the same diff size without performing highlighting.
|
|
|
|
#
|
|
|
|
opts = Gitlab::Diff::FileCollection::Base.default_options.merge(diff_options || {})
|
2016-12-26 14:27:20 +00:00
|
|
|
|
|
|
|
raw_diffs(opts).size
|
2016-03-03 17:38:44 +00:00
|
|
|
end
|
|
|
|
|
2016-01-20 17:44:27 +00:00
|
|
|
def diff_base_commit
|
2016-06-20 16:48:04 +00:00
|
|
|
if persisted?
|
2016-01-20 17:44:27 +00:00
|
|
|
merge_request_diff.base_commit
|
2016-08-08 20:00:11 +00:00
|
|
|
else
|
|
|
|
branch_merge_base_commit
|
2016-06-20 16:48:04 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
|
|
|
|
# but we need to get a commit for the "View file @ ..." link by deleted files,
|
|
|
|
# so we find the likely one if we can't get the actual one.
|
|
|
|
# This will not be the actual base commit if the target branch was merged into
|
|
|
|
# the source branch after the merge request was created, but it is good enough
|
|
|
|
# for the specific purpose of linking to a commit.
|
2016-07-06 23:28:13 +00:00
|
|
|
# It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
|
|
|
|
# true base commit, so we can't simply have `#diff_base_commit` fall back on
|
|
|
|
# this method.
|
2016-06-20 16:48:04 +00:00
|
|
|
def likely_diff_base_commit
|
2016-12-28 20:01:36 +00:00
|
|
|
first_commit.try(:parent) || first_commit
|
2016-06-20 16:48:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def diff_start_commit
|
|
|
|
if persisted?
|
|
|
|
merge_request_diff.start_commit
|
|
|
|
else
|
|
|
|
target_branch_head
|
2016-01-20 17:44:27 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-06-20 16:48:04 +00: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
|
|
|
|
source_branch_ref = @source_branch_sha || source_branch
|
2016-08-25 12:46:59 +00:00
|
|
|
source_project.repository.commit(source_branch_ref) if source_branch_ref
|
2016-06-20 16:48:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def target_branch_head
|
|
|
|
target_branch_ref = @target_branch_sha || target_branch
|
2016-08-25 12:46:59 +00:00
|
|
|
target_project.repository.commit(target_branch_ref) if target_branch_ref
|
2016-06-20 16:48:04 +00:00
|
|
|
end
|
|
|
|
|
2016-08-08 20:00:11 +00: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 16:48:04 +00:00
|
|
|
def target_branch_sha
|
2016-07-31 22:44:02 +00:00
|
|
|
@target_branch_sha || target_branch_head.try(:sha)
|
2016-06-20 16:48:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def source_branch_sha
|
2016-07-31 22:44:02 +00:00
|
|
|
@source_branch_sha || source_branch_head.try(:sha)
|
2015-10-20 12:23:56 +00:00
|
|
|
end
|
|
|
|
|
2016-08-22 11:18:58 +00:00
|
|
|
def diff_refs
|
|
|
|
return unless diff_start_commit || diff_base_commit
|
|
|
|
|
|
|
|
Gitlab::Diff::DiffRefs.new(
|
|
|
|
base_sha: diff_base_sha,
|
|
|
|
start_sha: diff_start_sha,
|
|
|
|
head_sha: diff_head_sha
|
|
|
|
)
|
2016-08-08 20:00:11 +00:00
|
|
|
end
|
|
|
|
|
2016-08-22 11:18:58 +00:00
|
|
|
# Return diff_refs instance trying to not touch the git repository
|
|
|
|
def diff_sha_refs
|
2016-08-15 15:11:18 +00:00
|
|
|
if merge_request_diff && merge_request_diff.diff_refs_by_sha?
|
2016-07-28 20:40:13 +00:00
|
|
|
merge_request_diff.diff_refs
|
2016-08-08 20:00:11 +00:00
|
|
|
else
|
2016-08-22 11:18:58 +00:00
|
|
|
diff_refs
|
2016-07-28 20:40:13 +00:00
|
|
|
end
|
2016-06-20 16:51:48 +00:00
|
|
|
end
|
|
|
|
|
2016-08-22 11:18:58 +00:00
|
|
|
def branch_merge_base_sha
|
|
|
|
branch_merge_base_commit.try(:sha)
|
|
|
|
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?
|
2016-04-12 18:39:33 +00: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 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,
|
2015-12-15 02:53:52 +00:00
|
|
|
"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
|
2016-07-26 11:57:43 +00:00
|
|
|
return true if target_project == source_project
|
2016-10-19 18:33:51 +00:00
|
|
|
return true unless source_project_missing?
|
2014-04-03 07:36:10 +00:00
|
|
|
|
2016-07-26 11:57:43 +00:00
|
|
|
errors.add :validate_fork,
|
2016-08-25 13:08:31 +00:00
|
|
|
'Source project is not a fork of the target project'
|
2016-07-26 11:57:43 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def closed_without_fork?
|
2016-10-19 18:33:51 +00:00
|
|
|
closed? && source_project_missing?
|
2016-07-26 11:57:43 +00:00
|
|
|
end
|
|
|
|
|
2016-10-19 18:33:51 +00:00
|
|
|
def source_project_missing?
|
2016-07-26 11:57:43 +00:00
|
|
|
return false unless for_fork?
|
|
|
|
return true unless source_project
|
|
|
|
|
|
|
|
!source_project.forked_from?(target_project)
|
2014-04-02 18:51:53 +00:00
|
|
|
end
|
|
|
|
|
2016-09-13 12:40:00 +00:00
|
|
|
def reopenable?
|
2016-10-19 16:13:04 +00:00
|
|
|
closed? && !source_project_missing? && source_branch_exists?
|
2016-09-06 14:48:59 +00:00
|
|
|
end
|
|
|
|
|
2016-08-02 12:38:03 +00:00
|
|
|
def ensure_merge_request_diff
|
|
|
|
merge_request_diff || create_merge_request_diff
|
2016-07-25 14:04:55 +00:00
|
|
|
end
|
|
|
|
|
2016-08-02 13:28:37 +00: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
|
|
|
|
|
2016-08-02 12:38:03 +00:00
|
|
|
def reload_diff_if_branch_changed
|
2014-02-18 18:17:26 +00:00
|
|
|
if source_branch_changed? || target_branch_changed?
|
2016-06-20 17:22:39 +00:00
|
|
|
reload_diff
|
2014-02-18 18:17:26 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-06-20 17:22:39 +00:00
|
|
|
def reload_diff
|
2016-07-25 11:10:21 +00:00
|
|
|
return unless open?
|
|
|
|
|
2016-06-20 17:22:39 +00:00
|
|
|
old_diff_refs = self.diff_refs
|
2016-07-25 15:42:57 +00:00
|
|
|
create_merge_request_diff
|
2016-07-20 16:25:36 +00:00
|
|
|
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
|
2016-06-20 17:22:39 +00:00
|
|
|
new_diff_refs = self.diff_refs
|
|
|
|
|
|
|
|
update_diff_notes_positions(
|
|
|
|
old_diff_refs: old_diff_refs,
|
|
|
|
new_diff_refs: new_diff_refs
|
|
|
|
)
|
2012-03-15 23:45:46 +00:00
|
|
|
end
|
|
|
|
|
2012-03-30 05:05:04 +00:00
|
|
|
def check_if_can_be_merged
|
2016-01-05 15:24:42 +00:00
|
|
|
return unless unchecked?
|
|
|
|
|
2015-08-11 12:33:31 +00:00
|
|
|
can_be_merged =
|
2016-06-20 16:48:04 +00:00
|
|
|
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
|
2015-08-11 12:33:31 +00:00
|
|
|
|
|
|
|
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
|
2016-07-08 10:35:31 +00:00
|
|
|
@merge_event ||= 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
|
2016-07-08 10:35:31 +00:00
|
|
|
@closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
|
2012-03-15 23:45:46 +00:00
|
|
|
end
|
|
|
|
|
2015-04-30 13:43:32 +00:00
|
|
|
def work_in_progress?
|
2016-09-08 09:18:41 +00:00
|
|
|
self.class.work_in_progress?(title)
|
2016-02-27 03:34:14 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def wipless_title
|
2016-09-08 09:18:41 +00:00
|
|
|
self.class.wipless_title(self.title)
|
|
|
|
end
|
|
|
|
|
|
|
|
def wip_title
|
|
|
|
self.class.wip_title(self.title)
|
2015-04-30 13:43:32 +00:00
|
|
|
end
|
|
|
|
|
2016-06-24 16:13:56 +00:00
|
|
|
def mergeable?(skip_ci_check: false)
|
|
|
|
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
|
2016-01-05 15:24:42 +00:00
|
|
|
|
|
|
|
check_if_can_be_merged
|
|
|
|
|
|
|
|
can_be_merged?
|
2015-04-30 13:43:32 +00:00
|
|
|
end
|
|
|
|
|
2016-06-24 16:13:56 +00:00
|
|
|
def mergeable_state?(skip_ci_check: false)
|
2016-06-07 11:01:34 +00:00
|
|
|
return false unless open?
|
|
|
|
return false if work_in_progress?
|
|
|
|
return false if broken?
|
2016-06-24 16:13:56 +00:00
|
|
|
return false unless skip_ci_check || mergeable_ci_state?
|
2016-09-16 11:02:42 +00:00
|
|
|
return false unless mergeable_discussions_state?
|
2016-06-07 11:01:34 +00:00
|
|
|
|
|
|
|
true
|
2015-04-30 13:43:32 +00:00
|
|
|
end
|
|
|
|
|
2017-02-17 13:56:13 +00:00
|
|
|
def can_cancel_merge_when_pipeline_succeeds?(current_user)
|
2015-11-24 13:59:02 +00:00
|
|
|
can_be_merged_by?(current_user) || self.author == current_user
|
2015-11-23 08:42:20 +00:00
|
|
|
end
|
|
|
|
|
2015-11-24 13:59:02 +00:00
|
|
|
def can_remove_source_branch?(current_user)
|
|
|
|
!source_project.protected_branch?(source_branch) &&
|
|
|
|
!source_project.root_ref?(source_branch) &&
|
2016-08-08 18:55:13 +00:00
|
|
|
Ability.allowed?(current_user, :push_code, source_project) &&
|
2016-06-20 16:48:04 +00:00
|
|
|
diff_head_commit == source_branch_head
|
2015-11-18 10:17:41 +00:00
|
|
|
end
|
|
|
|
|
2016-02-12 20:41:31 +00:00
|
|
|
def should_remove_source_branch?
|
2016-11-03 14:15:03 +00:00
|
|
|
Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
|
2016-02-12 20:41:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def force_remove_source_branch?
|
2016-11-03 14:15:03 +00:00
|
|
|
Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
|
2016-02-12 20:41:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def remove_source_branch?
|
|
|
|
should_remove_source_branch? || force_remove_source_branch?
|
|
|
|
end
|
|
|
|
|
2016-12-09 01:56:31 +00:00
|
|
|
def related_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" +
|
2015-10-22 16:38:00 +00: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 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
|
|
|
|
2016-07-26 04:43:47 +00:00
|
|
|
def discussions
|
2016-12-09 01:56:31 +00:00
|
|
|
@discussions ||= self.related_notes.
|
2016-08-18 23:27:22 +00:00
|
|
|
inc_relations_for_view.
|
2016-07-26 04:43:47 +00:00
|
|
|
fresh.
|
|
|
|
discussions
|
|
|
|
end
|
|
|
|
|
2016-08-17 17:14:44 +00:00
|
|
|
def diff_discussions
|
|
|
|
@diff_discussions ||= self.notes.diff_notes.discussions
|
|
|
|
end
|
|
|
|
|
2016-10-26 21:21:50 +00:00
|
|
|
def resolvable_discussions
|
|
|
|
@resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?)
|
|
|
|
end
|
|
|
|
|
|
|
|
def discussions_can_be_resolved_by?(user)
|
|
|
|
resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) }
|
|
|
|
end
|
|
|
|
|
2016-08-17 17:14:44 +00:00
|
|
|
def find_diff_discussion(discussion_id)
|
|
|
|
notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
|
|
|
|
return if notes.empty?
|
|
|
|
|
|
|
|
Discussion.new(notes)
|
2016-08-12 21:24:09 +00:00
|
|
|
end
|
|
|
|
|
2016-07-29 03:14:45 +00:00
|
|
|
def discussions_resolvable?
|
2016-08-17 17:14:44 +00:00
|
|
|
diff_discussions.any?(&:resolvable?)
|
2016-07-29 03:14:45 +00:00
|
|
|
end
|
|
|
|
|
2016-07-29 02:39:35 +00:00
|
|
|
def discussions_resolved?
|
2016-08-17 17:14:44 +00:00
|
|
|
discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
|
2016-07-29 02:39:35 +00:00
|
|
|
end
|
|
|
|
|
2016-11-23 12:00:58 +00:00
|
|
|
def discussions_to_be_resolved?
|
|
|
|
discussions_resolvable? && !discussions_resolved?
|
|
|
|
end
|
|
|
|
|
2016-09-16 11:02:42 +00:00
|
|
|
def mergeable_discussions_state?
|
|
|
|
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
|
|
|
|
|
2016-11-23 12:00:58 +00:00
|
|
|
!discussions_to_be_resolved?
|
2016-09-16 11:02:42 +00:00
|
|
|
end
|
|
|
|
|
2014-09-15 07:10:35 +00:00
|
|
|
def hook_attrs
|
|
|
|
attrs = {
|
2016-04-12 18:39:33 +00:00
|
|
|
source: source_project.try(:hook_attrs),
|
2014-09-15 07:10:35 +00:00
|
|
|
target: target_project.hook_attrs,
|
2015-09-25 20:07:08 +00:00
|
|
|
last_commit: nil,
|
2017-03-14 16:56:15 +00: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 07:10:35 +00:00
|
|
|
}
|
|
|
|
|
2016-06-20 16:48:04 +00:00
|
|
|
if diff_head_commit
|
2017-02-21 23:50:22 +00:00
|
|
|
attrs[:last_commit] = diff_head_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
|
|
|
|
|
2017-03-21 15:25:00 +00: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 08:59:36 +00: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 14:53:44 +00:00
|
|
|
# This optimization does not apply to issues from external sources.
|
2017-01-31 14:42:54 +00:00
|
|
|
def cache_merge_request_closes_issues!(current_user)
|
2016-09-30 14:53:44 +00: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 08:59:36 +00:00
|
|
|
transaction do
|
2016-09-20 09:36:54 +00:00
|
|
|
self.merge_requests_closing_issues.delete_all
|
2016-09-30 14:53:44 +00: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 08:59:36 +00:00
|
|
|
closes_issues(current_user).each do |issue|
|
2016-09-20 12:43:11 +00: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 08:59:36 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
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
|
2017-02-13 15:04:06 +00:00
|
|
|
messages = [title, description]
|
2016-09-20 19:17:37 +00:00
|
|
|
messages.concat(commits.map(&:safe_message)) if merge_request_diff
|
2016-01-27 13:09:58 +00:00
|
|
|
|
|
|
|
Gitlab::ClosingIssueExtractor.new(project, current_user).
|
|
|
|
closed_by_message(messages.join("\n"))
|
2013-05-30 23:16:49 +00:00
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-01-31 14:42:54 +00:00
|
|
|
def issues_mentioned_but_not_closing(current_user)
|
2016-12-05 11:44:29 +00:00
|
|
|
return [] unless target_branch == project.default_branch
|
2016-11-23 21:50:49 +00:00
|
|
|
|
2016-12-05 11:44:29 +00:00
|
|
|
ext = Gitlab::ReferenceExtractor.new(project, current_user)
|
2017-02-13 15:04:06 +00:00
|
|
|
ext.analyze("#{title}\n#{description}")
|
2016-11-23 21:50:49 +00:00
|
|
|
|
2017-01-18 09:05:09 +00:00
|
|
|
ext.issues - closes_issues(current_user)
|
2016-11-23 21:50:49 +00:00
|
|
|
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
|
2017-02-14 14:03:24 +00:00
|
|
|
source_project.namespace.full_path
|
2014-02-15 12:36:58 +00:00
|
|
|
else
|
|
|
|
"(removed)"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-05-08 12:37:36 +00:00
|
|
|
def target_project_namespace
|
|
|
|
if target_project && target_project.namespace
|
2017-02-14 14:03:24 +00:00
|
|
|
target_project.namespace.full_path
|
2014-05-08 12:37:36 +00:00
|
|
|
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
|
|
|
|
|
2016-11-28 09:48:55 +00:00
|
|
|
def merge_commit_message(include_description: false)
|
2016-11-24 21:01:05 +00:00
|
|
|
closes_issues_references = closes_issues.map do |issue|
|
|
|
|
issue.to_reference(target_project)
|
|
|
|
end
|
|
|
|
|
2016-12-05 11:29:17 +00:00
|
|
|
message = [
|
|
|
|
"Merge branch '#{source_branch}' into '#{target_branch}'",
|
|
|
|
title
|
|
|
|
]
|
2016-11-24 21:01:05 +00:00
|
|
|
|
2016-12-05 11:29:17 +00:00
|
|
|
if !include_description && closes_issues_references.present?
|
2016-12-13 13:00:00 +00:00
|
|
|
message << "Closes #{closes_issues_references.to_sentence}"
|
2016-11-24 21:01:05 +00:00
|
|
|
end
|
|
|
|
|
2016-12-05 11:29:17 +00:00
|
|
|
message << "#{description}" if include_description && description.present?
|
2016-09-21 14:22:54 +00:00
|
|
|
message << "See merge request #{to_reference}"
|
|
|
|
|
2016-12-05 11:29:17 +00:00
|
|
|
message.join("\n\n")
|
2014-01-13 15:20:30 +00:00
|
|
|
end
|
2014-01-31 11:24:38 +00:00
|
|
|
|
2017-02-17 13:56:13 +00:00
|
|
|
def reset_merge_when_pipeline_succeeds
|
|
|
|
return unless merge_when_pipeline_succeeds?
|
2015-11-18 10:17:41 +00:00
|
|
|
|
2017-02-17 13:56:13 +00:00
|
|
|
self.merge_when_pipeline_succeeds = false
|
2015-11-02 16:27:38 +00:00
|
|
|
self.merge_user = nil
|
2016-02-12 20:41:31 +00:00
|
|
|
if merge_params
|
|
|
|
merge_params.delete('should_remove_source_branch')
|
|
|
|
merge_params.delete('commit_message')
|
|
|
|
end
|
2015-11-02 16:27:38 +00:00
|
|
|
|
|
|
|
self.save
|
|
|
|
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?
|
2017-02-14 22:08:30 +00: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 07:40:26 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def branch_missing?
|
|
|
|
!source_branch_exists? || !target_branch_exists?
|
|
|
|
end
|
2015-06-11 16:45:12 +00:00
|
|
|
|
2016-01-05 15:24:42 +00:00
|
|
|
def broken?
|
2016-12-01 11:17:30 +00:00
|
|
|
has_no_commits? || branch_missing? || cannot_be_merged?
|
2016-01-05 15:24:42 +00:00
|
|
|
end
|
|
|
|
|
2015-06-11 16:45:12 +00:00
|
|
|
def can_be_merged_by?(user)
|
2016-07-18 08:16:56 +00: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 05:36:32 +00:00
|
|
|
end
|
|
|
|
|
2016-06-09 14:08:38 +00:00
|
|
|
def mergeable_ci_state?
|
2017-02-17 13:56:13 +00:00
|
|
|
return true unless project.only_allow_merge_if_pipeline_succeeds?
|
2016-06-07 11:01:34 +00:00
|
|
|
|
2016-11-28 10:13:32 +00:00
|
|
|
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
|
2016-04-27 18:34:42 +00:00
|
|
|
end
|
|
|
|
|
2017-02-07 00:06:46 +00:00
|
|
|
def environments_for(current_user)
|
2016-09-16 08:16:21 +00:00
|
|
|
return [] unless diff_head_commit
|
2016-08-09 13:11:14 +00:00
|
|
|
|
2017-02-07 00:06:46 +00: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 13:46:36 +00:00
|
|
|
|
2017-02-07 00:06:46 +00:00
|
|
|
if source_project
|
|
|
|
envs.concat EnvironmentsFinder.new(source_project, current_user,
|
|
|
|
ref: source_branch, commit: diff_head_commit).execute
|
|
|
|
end
|
2016-11-09 13:46:36 +00:00
|
|
|
|
2017-02-07 00:06:46 +00:00
|
|
|
h[current_user] = envs.uniq
|
2016-11-09 13:46:36 +00:00
|
|
|
end
|
2017-02-07 00:06:46 +00:00
|
|
|
|
|
|
|
@environments[current_user]
|
2016-08-02 12:01:22 +00:00
|
|
|
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
|
|
|
|
2016-02-29 20:13:00 +00:00
|
|
|
def state_icon_name
|
|
|
|
if merged?
|
|
|
|
"check"
|
|
|
|
elsif closed?
|
|
|
|
"times"
|
|
|
|
else
|
|
|
|
"circle-o"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-08-11 12:33:31 +00:00
|
|
|
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
|
|
|
|
|
2016-07-03 23:58:58 +00:00
|
|
|
def ref_fetched?
|
|
|
|
project.repository.ref_exists?(ref_path)
|
2015-09-21 14:25:59 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def ensure_ref_fetched
|
2016-07-03 23:58:58 +00:00
|
|
|
fetch_ref unless ref_fetched?
|
2015-09-21 14:25:59 +00:00
|
|
|
end
|
|
|
|
|
2015-08-11 12:33:31 +00:00
|
|
|
def in_locked_state
|
|
|
|
begin
|
|
|
|
lock_mr
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
unlock_mr if locked?
|
|
|
|
end
|
|
|
|
end
|
2015-10-23 15:01:10 +00:00
|
|
|
|
2016-02-04 18:23:58 +00:00
|
|
|
def diverged_commits_count
|
|
|
|
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
|
|
|
|
|
2016-06-20 16:48:04 +00:00
|
|
|
if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
|
2016-02-04 18:23:58 +00:00
|
|
|
cache = {
|
2016-06-20 16:48:04 +00:00
|
|
|
source_sha: source_branch_sha,
|
|
|
|
target_sha: target_branch_sha,
|
2016-02-04 18:23:58 +00: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 16:48:04 +00:00
|
|
|
return 0 unless source_branch_sha && target_branch_sha
|
2016-03-18 17:16:04 +00:00
|
|
|
|
2016-06-20 16:48:04 +00:00
|
|
|
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
|
2016-02-04 18:23:58 +00:00
|
|
|
end
|
2016-03-18 17:16:04 +00:00
|
|
|
private :compute_diverged_commits_count
|
2016-02-04 18:23:58 +00:00
|
|
|
|
|
|
|
def diverged_from_target_branch?
|
|
|
|
diverged_commits_count > 0
|
|
|
|
end
|
|
|
|
|
2016-11-28 10:13:32 +00:00
|
|
|
def head_pipeline
|
2016-08-11 09:26:04 +00:00
|
|
|
return unless diff_head_sha && source_project
|
2016-08-16 13:04:06 +00:00
|
|
|
|
2016-11-28 10:13:32 +00:00
|
|
|
@head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
|
2016-01-14 21:38:37 +00:00
|
|
|
end
|
2016-02-15 03:36:30 +00:00
|
|
|
|
2016-08-12 14:22:48 +00:00
|
|
|
def all_pipelines
|
2016-11-28 11:03:53 +00:00
|
|
|
return Ci::Pipeline.none unless source_project
|
2016-09-19 15:51:27 +00:00
|
|
|
|
2016-10-17 12:15:06 +00:00
|
|
|
@all_pipelines ||= source_project.pipelines
|
2016-10-17 13:31:21 +00:00
|
|
|
.where(sha: all_commits_sha, ref: source_branch)
|
|
|
|
.order(id: :desc)
|
2016-09-20 09:36:21 +00:00
|
|
|
end
|
2016-09-19 15:51:27 +00:00
|
|
|
|
2016-09-20 11:18:48 +00:00
|
|
|
# Note that this could also return SHA from now dangling commits
|
2016-10-17 12:15:06 +00:00
|
|
|
#
|
2016-09-20 09:36:21 +00:00
|
|
|
def all_commits_sha
|
2016-10-17 12:15:06 +00:00
|
|
|
if persisted?
|
|
|
|
merge_request_diffs.flat_map(&:commits_sha).uniq
|
2016-10-18 10:05:47 +00:00
|
|
|
elsif compare_commits
|
|
|
|
compare_commits.to_a.reverse.map(&:id)
|
2016-10-17 12:15:06 +00:00
|
|
|
else
|
2016-10-18 10:05:47 +00:00
|
|
|
[diff_head_sha]
|
2016-10-17 12:15:06 +00:00
|
|
|
end
|
2016-08-12 14:22:48 +00:00
|
|
|
end
|
|
|
|
|
2016-02-15 03:36:30 +00:00
|
|
|
def merge_commit
|
|
|
|
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
|
|
|
|
end
|
|
|
|
|
2016-11-29 13:47:43 +00:00
|
|
|
def can_be_reverted?(current_user)
|
2016-02-18 16:58:04 +00:00
|
|
|
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
|
2016-02-15 03:36:30 +00:00
|
|
|
end
|
2016-04-20 11:30:13 +00:00
|
|
|
|
|
|
|
def can_be_cherry_picked?
|
|
|
|
merge_commit
|
|
|
|
end
|
2016-07-03 23:58:58 +00:00
|
|
|
|
2016-08-04 09:31:44 +00:00
|
|
|
def has_complete_diff_refs?
|
2016-08-22 11:18:58 +00:00
|
|
|
diff_sha_refs && diff_sha_refs.complete?
|
2016-06-20 17:22:08 +00:00
|
|
|
end
|
|
|
|
|
2016-06-20 17:22:39 +00:00
|
|
|
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
|
2016-08-04 09:31:44 +00:00
|
|
|
return unless has_complete_diff_refs?
|
2016-06-20 17:22:39 +00:00
|
|
|
return if new_diff_refs == old_diff_refs
|
|
|
|
|
|
|
|
active_diff_notes = self.notes.diff_notes.select do |note|
|
|
|
|
note.new_diff_note? && note.active?(old_diff_refs)
|
|
|
|
end
|
|
|
|
|
|
|
|
return if active_diff_notes.empty?
|
|
|
|
|
|
|
|
paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq
|
|
|
|
|
|
|
|
service = Notes::DiffPositionUpdateService.new(
|
|
|
|
self.project,
|
|
|
|
nil,
|
|
|
|
old_diff_refs: old_diff_refs,
|
|
|
|
new_diff_refs: new_diff_refs,
|
|
|
|
paths: paths
|
|
|
|
)
|
|
|
|
|
2016-12-30 15:30:01 +00:00
|
|
|
transaction do
|
|
|
|
active_diff_notes.each do |note|
|
|
|
|
service.execute(note)
|
|
|
|
Gitlab::Timeless.timeless(note, &:save)
|
|
|
|
end
|
2016-06-20 17:22:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-03 23:58:58 +00:00
|
|
|
def keep_around_commit
|
|
|
|
project.repository.keep_around(self.merge_commit_sha)
|
|
|
|
end
|
2016-08-04 09:31:44 +00:00
|
|
|
|
|
|
|
def conflicts
|
|
|
|
@conflicts ||= Gitlab::Conflict::FileCollection.new(self)
|
|
|
|
end
|
|
|
|
|
2016-08-11 03:36:58 +00:00
|
|
|
def conflicts_can_be_resolved_by?(user)
|
|
|
|
access = ::Gitlab::UserAccess.new(user, project: source_project)
|
|
|
|
access.can_push_to_branch?(source_branch)
|
|
|
|
end
|
|
|
|
|
2016-08-11 03:36:13 +00:00
|
|
|
def conflicts_can_be_resolved_in_ui?
|
|
|
|
return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
|
|
|
|
|
|
|
|
return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
|
|
|
|
return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
|
2016-08-04 09:31:44 +00:00
|
|
|
|
|
|
|
begin
|
2016-08-25 11:32:35 +00:00
|
|
|
# Try to parse each conflict. If the MR's mergeable status hasn't been updated,
|
|
|
|
# ensure that we don't say there are conflicts to resolve when there are no conflict
|
|
|
|
# files.
|
|
|
|
conflicts.files.each(&:lines)
|
|
|
|
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
|
2016-09-01 12:59:10 +00:00
|
|
|
rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
|
2016-08-11 03:36:13 +00:00
|
|
|
@conflicts_can_be_resolved_in_ui = false
|
2016-08-04 09:31:44 +00:00
|
|
|
end
|
|
|
|
end
|
2016-12-01 11:17:30 +00:00
|
|
|
|
|
|
|
def has_commits?
|
2016-11-24 14:05:15 +00:00
|
|
|
merge_request_diff && commits_count > 0
|
2016-12-01 11:17:30 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def has_no_commits?
|
|
|
|
!has_commits?
|
|
|
|
end
|
2016-11-24 14:05:15 +00:00
|
|
|
|
|
|
|
def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
|
|
|
|
return false unless can_be_merged_by?(current_user)
|
|
|
|
|
|
|
|
return true if autocomplete_precheck
|
|
|
|
|
|
|
|
return false unless mergeable?(skip_ci_check: true)
|
|
|
|
return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
|
|
|
|
return false if last_diff_sha != diff_head_sha
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
2011-11-28 07:39:43 +00:00
|
|
|
end
|