2011-11-28 02:39:43 -05:00
|
|
|
class MergeRequest < ActiveRecord::Base
|
2018-03-06 14:06:27 -05:00
|
|
|
include NonatomicInternalId
|
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
|
2017-06-09 14:34:58 -04:00
|
|
|
include IgnorableColumn
|
2017-11-01 13:35:14 -04:00
|
|
|
include TimeTrackable
|
Use latest_merge_request_diff association
Compared to the merge_request_diff association:
1. It's simpler to query. The query uses a foreign key to the
merge_request_diffs table, so no ordering is necessary.
2. It's faster for preloading. The merge_request_diff association has to load
every diff for the MRs in the set, then discard all but the most recent for
each. This association means that Rails can just query for N diffs from N
MRs.
3. It's more complicated to update. This is a bidirectional foreign key, so we
need to update two tables when adding a diff record. This also means we need
to handle this as a special case when importing a GitLab project.
There is some juggling with this association in the merge request model:
* `MergeRequest#latest_merge_request_diff` is _always_ the latest diff.
* `MergeRequest#merge_request_diff` reuses
`MergeRequest#latest_merge_request_diff` unless:
* Arguments are passed. These are typically to force-reload the association.
* It doesn't exist. That means we might be trying to implicitly create a
diff. This only seems to happen in specs.
* The association is already loaded. This is important for the reasons
explained in the comment, which I'll reiterate here: if we a) load a
non-latest diff, then b) get its `merge_request`, then c) get that MR's
`merge_request_diff`, we should get the diff we loaded in c), even though
that's not the latest diff.
Basically, `MergeRequest#merge_request_diff` is the latest diff in most cases,
but not quite all.
2017-11-15 12:22:18 -05:00
|
|
|
include ManualInverseAssociation
|
|
|
|
include EachBatch
|
2017-12-01 08:52:16 -05:00
|
|
|
include ThrottledTouch
|
2017-12-11 10:38:16 -05:00
|
|
|
include Gitlab::Utils::StrongMemoize
|
2017-06-09 14:34:58 -04:00
|
|
|
|
2017-11-06 09:03:11 -05:00
|
|
|
ignore_column :locked_at,
|
2018-01-02 11:21:28 -05:00
|
|
|
:ref_fetched,
|
|
|
|
:deleted_at
|
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
|
|
|
|
2017-05-30 07:54:59 -04:00
|
|
|
has_many :merge_request_diffs
|
Use latest_merge_request_diff association
Compared to the merge_request_diff association:
1. It's simpler to query. The query uses a foreign key to the
merge_request_diffs table, so no ordering is necessary.
2. It's faster for preloading. The merge_request_diff association has to load
every diff for the MRs in the set, then discard all but the most recent for
each. This association means that Rails can just query for N diffs from N
MRs.
3. It's more complicated to update. This is a bidirectional foreign key, so we
need to update two tables when adding a diff record. This also means we need
to handle this as a special case when importing a GitLab project.
There is some juggling with this association in the merge request model:
* `MergeRequest#latest_merge_request_diff` is _always_ the latest diff.
* `MergeRequest#merge_request_diff` reuses
`MergeRequest#latest_merge_request_diff` unless:
* Arguments are passed. These are typically to force-reload the association.
* It doesn't exist. That means we might be trying to implicitly create a
diff. This only seems to happen in specs.
* The association is already loaded. This is important for the reasons
explained in the comment, which I'll reiterate here: if we a) load a
non-latest diff, then b) get its `merge_request`, then c) get that MR's
`merge_request_diff`, we should get the diff we loaded in c), even though
that's not the latest diff.
Basically, `MergeRequest#merge_request_diff` is the latest diff in most cases,
but not quite all.
2017-11-15 12:22:18 -05:00
|
|
|
|
2016-08-02 09:28:37 -04:00
|
|
|
has_one :merge_request_diff,
|
2017-07-07 18:03:10 -04:00
|
|
|
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
|
2016-08-02 09:28:37 -04:00
|
|
|
|
Use latest_merge_request_diff association
Compared to the merge_request_diff association:
1. It's simpler to query. The query uses a foreign key to the
merge_request_diffs table, so no ordering is necessary.
2. It's faster for preloading. The merge_request_diff association has to load
every diff for the MRs in the set, then discard all but the most recent for
each. This association means that Rails can just query for N diffs from N
MRs.
3. It's more complicated to update. This is a bidirectional foreign key, so we
need to update two tables when adding a diff record. This also means we need
to handle this as a special case when importing a GitLab project.
There is some juggling with this association in the merge request model:
* `MergeRequest#latest_merge_request_diff` is _always_ the latest diff.
* `MergeRequest#merge_request_diff` reuses
`MergeRequest#latest_merge_request_diff` unless:
* Arguments are passed. These are typically to force-reload the association.
* It doesn't exist. That means we might be trying to implicitly create a
diff. This only seems to happen in specs.
* The association is already loaded. This is important for the reasons
explained in the comment, which I'll reiterate here: if we a) load a
non-latest diff, then b) get its `merge_request`, then c) get that MR's
`merge_request_diff`, we should get the diff we loaded in c), even though
that's not the latest diff.
Basically, `MergeRequest#merge_request_diff` is the latest diff in most cases,
but not quite all.
2017-11-15 12:22:18 -05:00
|
|
|
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
|
|
|
|
manual_inverse_association :latest_merge_request_diff, :merge_request
|
|
|
|
|
|
|
|
# This is the same as latest_merge_request_diff unless:
|
|
|
|
# 1. There are arguments - in which case we might be trying to force-reload.
|
|
|
|
# 2. This association is already loaded.
|
|
|
|
# 3. The latest diff does not exist.
|
|
|
|
#
|
|
|
|
# The second one in particular is important - MergeRequestDiff#merge_request
|
|
|
|
# is the inverse of MergeRequest#merge_request_diff, which means it may not be
|
|
|
|
# the latest diff, because we could have loaded any diff from this particular
|
|
|
|
# MR. If we haven't already loaded a diff, then it's fine to load the latest.
|
|
|
|
def merge_request_diff(*args)
|
|
|
|
fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?
|
|
|
|
|
|
|
|
fallback || super
|
|
|
|
end
|
|
|
|
|
2017-03-17 18:14:27 -04:00
|
|
|
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
|
|
|
|
|
2017-06-08 11:16:27 -04:00
|
|
|
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
2016-07-01 09:34:10 -04:00
|
|
|
|
2017-06-08 11:16:27 -04:00
|
|
|
has_many :merge_requests_closing_issues,
|
|
|
|
class_name: 'MergeRequestsClosingIssues',
|
|
|
|
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
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-07-03 10:01:41 -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?
|
2017-12-11 10:38:16 -05:00
|
|
|
after_update :clear_memoized_shas
|
2017-12-13 12:14:51 -05:00
|
|
|
after_update :reload_diff_if_branch_changed
|
2014-01-22 08:20:20 -05: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
|
2017-07-19 10:03:50 -04:00
|
|
|
transition [:opened] => :closed
|
2013-02-18 03:40:56 -05:00
|
|
|
end
|
|
|
|
|
2015-08-11 08:33:31 -04:00
|
|
|
event :mark_as_merged do
|
2017-07-19 10:03:50 -04:00
|
|
|
transition [:opened, :locked] => :merged
|
2013-02-18 03:40:56 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
event :reopen do
|
2017-07-19 10:03:50 -04:00
|
|
|
transition closed: :opened
|
2013-02-18 03:40:56 -05:00
|
|
|
end
|
|
|
|
|
2014-06-26 08:09:17 -04:00
|
|
|
event :lock_mr do
|
2017-07-19 10:03:50 -04:00
|
|
|
transition [:opened] => :locked
|
2014-01-24 07:18:32 -05:00
|
|
|
end
|
|
|
|
|
2014-06-26 08:09:17 -04:00
|
|
|
event :unlock_mr do
|
2017-07-19 10:03:50 -04:00
|
|
|
transition locked: :opened
|
2014-01-24 07:18:32 -05:00
|
|
|
end
|
|
|
|
|
2017-12-11 16:00:11 -05:00
|
|
|
before_transition any => :opened do |merge_request|
|
|
|
|
merge_request.merge_jid = nil
|
|
|
|
|
|
|
|
merge_request.run_after_commit do
|
|
|
|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-18 03:40:56 -05:00
|
|
|
state :opened
|
|
|
|
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) }
|
2018-01-12 15:38:36 -05:00
|
|
|
scope :by_commit_sha, ->(sha) do
|
|
|
|
where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
|
|
|
|
end
|
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
|
|
|
|
|
2017-12-20 04:01:21 -05:00
|
|
|
def rebase_in_progress?
|
2018-02-08 01:23:15 -05:00
|
|
|
strong_memoize(:rebase_in_progress) do
|
|
|
|
# The source project can be deleted
|
|
|
|
next false unless source_project
|
2017-12-20 04:01:21 -05:00
|
|
|
|
2018-02-08 01:23:15 -05:00
|
|
|
source_project.repository.rebase_in_progress?(id)
|
|
|
|
end
|
2017-12-20 04:01:21 -05:00
|
|
|
end
|
|
|
|
|
2017-11-30 13:00:09 -05:00
|
|
|
# Use this method whenever you need to make sure the head_pipeline is synced with the
|
|
|
|
# branch head commit, for example checking if a merge request can be merged.
|
|
|
|
# For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
|
2017-12-01 08:26:51 -05:00
|
|
|
def actual_head_pipeline
|
2017-11-30 13:00:09 -05:00
|
|
|
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
|
2017-09-07 03:58:15 -04:00
|
|
|
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])
|
|
|
|
|
2017-08-03 22:20:34 -04:00
|
|
|
where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
|
2016-03-07 06:56:46 -05:00
|
|
|
end
|
|
|
|
|
Use latest_merge_request_diff association
Compared to the merge_request_diff association:
1. It's simpler to query. The query uses a foreign key to the
merge_request_diffs table, so no ordering is necessary.
2. It's faster for preloading. The merge_request_diff association has to load
every diff for the MRs in the set, then discard all but the most recent for
each. This association means that Rails can just query for N diffs from N
MRs.
3. It's more complicated to update. This is a bidirectional foreign key, so we
need to update two tables when adding a diff record. This also means we need
to handle this as a special case when importing a GitLab project.
There is some juggling with this association in the merge request model:
* `MergeRequest#latest_merge_request_diff` is _always_ the latest diff.
* `MergeRequest#merge_request_diff` reuses
`MergeRequest#latest_merge_request_diff` unless:
* Arguments are passed. These are typically to force-reload the association.
* It doesn't exist. That means we might be trying to implicitly create a
diff. This only seems to happen in specs.
* The association is already loaded. This is important for the reasons
explained in the comment, which I'll reiterate here: if we a) load a
non-latest diff, then b) get its `merge_request`, then c) get that MR's
`merge_request_diff`, we should get the diff we loaded in c), even though
that's not the latest diff.
Basically, `MergeRequest#merge_request_diff` is the latest diff in most cases,
but not quite all.
2017-11-15 12:22:18 -05:00
|
|
|
# This is used after project import, to reset the IDs to the correct
|
|
|
|
# values. It is not intended to be called without having already scoped the
|
|
|
|
# relation.
|
|
|
|
def self.set_latest_merge_request_diff_ids!
|
|
|
|
update = '
|
|
|
|
latest_merge_request_diff_id = (
|
|
|
|
SELECT MAX(id)
|
|
|
|
FROM merge_request_diffs
|
|
|
|
WHERE merge_requests.id = merge_request_diffs.merge_request_id
|
|
|
|
)'.squish
|
|
|
|
|
|
|
|
self.each_batch do |batch|
|
|
|
|
batch.update_all(update)
|
|
|
|
end
|
|
|
|
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-11-21 14:25:37 -05:00
|
|
|
# Verifies if title has changed not taking into account WIP prefix
|
|
|
|
# for merge requests.
|
|
|
|
def wipless_title_changed(old_title)
|
|
|
|
self.class.wipless_title(old_title) != self.wipless_title
|
|
|
|
end
|
|
|
|
|
2017-09-19 13:23:15 -04:00
|
|
|
def hook_attrs
|
2017-10-05 13:02:50 -04:00
|
|
|
Gitlab::HookData::MergeRequestBuilder.new(self).build
|
2017-09-19 13:23:15 -04:00
|
|
|
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
|
|
|
|
|
2017-06-20 15:32:49 -04:00
|
|
|
# These method are needed for compatibility with issues to not mess view and other code
|
2017-05-04 08:11:15 -04:00
|
|
|
def assignees
|
|
|
|
Array(assignee)
|
|
|
|
end
|
|
|
|
|
2017-06-20 15:32:49 -04:00
|
|
|
def assignee_ids
|
|
|
|
Array(assignee_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def assignee_ids=(ids)
|
|
|
|
write_attribute(:assignee_id, ids.last)
|
|
|
|
end
|
|
|
|
|
2017-05-04 08:11:15 -04:00
|
|
|
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
|
|
|
|
|
2017-07-13 21:29:53 -04:00
|
|
|
def commits
|
|
|
|
if persisted?
|
|
|
|
merge_request_diff.commits
|
|
|
|
elsif compare_commits
|
|
|
|
compare_commits.reverse
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def commits_count
|
|
|
|
if persisted?
|
|
|
|
merge_request_diff.commits_count
|
|
|
|
elsif compare_commits
|
|
|
|
compare_commits.size
|
|
|
|
else
|
|
|
|
0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def commit_shas
|
|
|
|
if persisted?
|
|
|
|
merge_request_diff.commit_shas
|
|
|
|
elsif compare_commits
|
2017-11-21 11:58:08 -05:00
|
|
|
compare_commits.to_a.reverse.map(&:sha)
|
2017-07-13 21:29:53 -04:00
|
|
|
else
|
2017-11-21 11:58:08 -05:00
|
|
|
Array(diff_head_sha)
|
2017-07-13 21:29:53 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-10 17:01:01 -04:00
|
|
|
# Calls `MergeWorker` to proceed with the merge process and
|
|
|
|
# updates `merge_jid` with the MergeWorker#jid.
|
|
|
|
# This helps tracking enqueued and ongoing merge jobs.
|
2017-08-28 14:55:25 -04:00
|
|
|
def merge_async(user_id, params)
|
2017-08-10 17:01:01 -04:00
|
|
|
jid = MergeWorker.perform_async(id, user_id, params)
|
|
|
|
update_column(:merge_jid, jid)
|
|
|
|
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.
|
2017-07-13 21:29:53 -04:00
|
|
|
merge_request_diff&.real_size || 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
|
2018-03-05 22:03:48 -05:00
|
|
|
if persisted?
|
|
|
|
merge_request_diff.start_commit_sha
|
|
|
|
else
|
|
|
|
target_branch_head.try(:sha)
|
|
|
|
end
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def diff_base_sha
|
2018-03-05 22:03:48 -05:00
|
|
|
if persisted?
|
|
|
|
merge_request_diff.base_commit_sha
|
|
|
|
else
|
|
|
|
branch_merge_base_commit.try(:sha)
|
|
|
|
end
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def diff_head_sha
|
2018-03-05 22:03:48 -05:00
|
|
|
if persisted?
|
|
|
|
merge_request_diff.head_commit_sha
|
|
|
|
else
|
|
|
|
source_branch_head.try(:sha)
|
|
|
|
end
|
2016-06-20 12:48:04 -04:00
|
|
|
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
|
|
|
|
|
2017-11-24 06:58:05 -05:00
|
|
|
def source_branch_ref
|
|
|
|
return @source_branch_sha if @source_branch_sha
|
|
|
|
return unless source_branch
|
|
|
|
|
|
|
|
Gitlab::Git::BRANCH_REF_PREFIX + source_branch
|
|
|
|
end
|
|
|
|
|
|
|
|
def target_branch_ref
|
|
|
|
return @target_branch_sha if @target_branch_sha
|
|
|
|
return unless target_branch
|
|
|
|
|
|
|
|
Gitlab::Git::BRANCH_REF_PREFIX + target_branch
|
|
|
|
end
|
|
|
|
|
2016-06-20 12:48:04 -04:00
|
|
|
def source_branch_head
|
2017-12-11 10:38:16 -05:00
|
|
|
strong_memoize(:source_branch_head) do
|
|
|
|
if source_project && source_branch_ref
|
|
|
|
source_project.repository.commit(source_branch_ref)
|
|
|
|
end
|
|
|
|
end
|
2016-06-20 12:48:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def target_branch_head
|
2017-12-11 10:38:16 -05:00
|
|
|
strong_memoize(:target_branch_head) do
|
|
|
|
target_project.repository.commit(target_branch_ref)
|
|
|
|
end
|
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
|
|
|
|
2017-07-19 10:03:50 -04:00
|
|
|
if opened?
|
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
|
|
|
|
|
2017-07-31 18:01:36 -04:00
|
|
|
def merge_ongoing?
|
2017-10-27 16:11:21 -04:00
|
|
|
# While the MergeRequest is locked, it should present itself as 'merge ongoing'.
|
|
|
|
# The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
|
2017-10-30 06:21:23 -04:00
|
|
|
return true if locked?
|
|
|
|
|
|
|
|
!!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
|
2017-07-31 18:01:36 -04:00
|
|
|
end
|
|
|
|
|
2016-07-26 07:57:43 -04:00
|
|
|
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
|
|
|
|
|
2017-09-20 11:41:11 -04:00
|
|
|
!source_project.in_fork_network_of?(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
|
2017-11-01 10:31:35 -04:00
|
|
|
fetch_ref!
|
2017-09-22 11:46:24 -04:00
|
|
|
|
2017-09-19 06:55:37 -04:00
|
|
|
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
|
|
|
|
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
|
|
|
merge_request_diffs.create
|
|
|
|
reload_merge_request_diff
|
|
|
|
end
|
2016-08-02 09:28:37 -04:00
|
|
|
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|
|
2017-11-21 11:58:08 -05:00
|
|
|
diffs = merge_request_diffs.viewable
|
2017-04-30 16:32:09 -04:00
|
|
|
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
|
|
|
|
|
2017-12-11 10:38:16 -05:00
|
|
|
def clear_memoized_shas
|
|
|
|
@target_branch_sha = @source_branch_sha = nil
|
|
|
|
|
|
|
|
clear_memoization(:source_branch_head)
|
|
|
|
clear_memoization(:target_branch_head)
|
|
|
|
end
|
|
|
|
|
2016-08-02 08:38:03 -04:00
|
|
|
def reload_diff_if_branch_changed
|
2017-08-10 11:53:55 -04:00
|
|
|
if (source_branch_changed? || target_branch_changed?) &&
|
|
|
|
(source_branch_head && target_branch_head)
|
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
|
2018-03-14 12:03:10 -04:00
|
|
|
new_diff = create_merge_request_diff
|
|
|
|
|
|
|
|
MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
|
2017-09-22 11:46:24 -04:00
|
|
|
|
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
|
2017-09-19 03:44:58 -04:00
|
|
|
return unless unchecked? && Gitlab::Database.read_write?
|
2016-01-05 10:24:42 -05:00
|
|
|
|
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
|
|
|
|
|
2017-12-20 04:01:21 -05:00
|
|
|
can_be_merged? && !should_be_rebased?
|
2015-04-30 09:43:32 -04:00
|
|
|
end
|
|
|
|
|
2018-01-29 20:53:47 -05:00
|
|
|
def mergeable_state?(skip_ci_check: false, skip_discussions_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?
|
2018-01-29 20:53:47 -05:00
|
|
|
return false unless skip_discussions_check || mergeable_discussions_state?
|
2016-06-07 07:01:34 -04:00
|
|
|
|
|
|
|
true
|
2015-04-30 09:43:32 -04:00
|
|
|
end
|
|
|
|
|
2017-09-29 15:13:35 -04:00
|
|
|
def ff_merge_possible?
|
|
|
|
project.repository.ancestor?(target_branch_sha, diff_head_sha)
|
|
|
|
end
|
|
|
|
|
|
|
|
def should_be_rebased?
|
|
|
|
project.ff_merge_must_be_possible? && !ff_merge_possible?
|
|
|
|
end
|
|
|
|
|
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) &&
|
2018-03-05 22:03:48 -05:00
|
|
|
diff_head_sha == source_branch_head.try(:sha)
|
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
|
2017-06-16 10:00:58 -04:00
|
|
|
commit_ids = commit_shas.take(commits_for_notes_limit)
|
2014-01-22 08:54:53 -05:00
|
|
|
|
2017-10-02 04:14:23 -04:00
|
|
|
commit_notes = Note
|
|
|
|
.except(:order)
|
|
|
|
.where(project_id: [source_project_id, target_project_id])
|
2017-11-07 11:11:37 -05:00
|
|
|
.for_commit_id(commit_ids)
|
2017-10-02 04:14:23 -04:00
|
|
|
|
|
|
|
# We're using a UNION ALL here since this results in better performance
|
|
|
|
# compared to using OR statements. We're using UNION ALL since the queries
|
|
|
|
# used won't produce any duplicates (e.g. a note for a commit can't also be
|
|
|
|
# a note for an MR).
|
|
|
|
union = Gitlab::SQL::Union
|
|
|
|
.new([notes, commit_notes], remove_duplicates: false)
|
|
|
|
.to_sql
|
|
|
|
|
|
|
|
Note.from("(#{union}) #{Note.table_name}")
|
2017-11-22 09:48:09 -05:00
|
|
|
.includes(:noteable)
|
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
|
|
|
|
|
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)
|
2017-07-10 03:38:42 -04:00
|
|
|
return unless project.issues_enabled?
|
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
|
|
|
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|
|
2017-08-29 14:36:35 -04:00
|
|
|
next if issue.is_a?(ExternalIssue)
|
|
|
|
|
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
|
2017-07-20 05:34:09 -04:00
|
|
|
target_project.full_path
|
2013-12-12 04:34:42 -05:00
|
|
|
else
|
|
|
|
"(removed)"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def source_project_path
|
|
|
|
if source_project
|
2017-07-20 05:34:09 -04:00
|
|
|
source_project.full_path
|
2013-12-12 04:34:42 -05:00
|
|
|
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
|
|
|
|
|
2017-10-10 10:56:04 -04:00
|
|
|
self.source_project.repository.branch_exists?(self.source_branch)
|
2013-12-12 04:34:42 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def target_branch_exists?
|
|
|
|
return false unless self.target_project
|
|
|
|
|
2017-10-10 10:56:04 -04:00
|
|
|
self.target_project.repository.branch_exists?(self.target_branch)
|
2013-12-12 04:34:42 -05:00
|
|
|
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
|
2018-01-11 11:34:01 -05:00
|
|
|
|
2016-12-05 06:29:17 -05:00
|
|
|
message << "#{description}" if include_description && description.present?
|
2017-08-13 03:28:19 -04:00
|
|
|
message << "See merge request #{to_reference(full: true)}"
|
2016-09-21 10:22:54 -04:00
|
|
|
|
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
|
|
|
|
2015-06-11 03:40:26 -04:00
|
|
|
def has_ci?
|
2017-10-03 17:13:13 -04:00
|
|
|
return false if has_no_commits?
|
2017-02-14 17:08:30 -05:00
|
|
|
|
2017-10-03 17:13:13 -04:00
|
|
|
!!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
|
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)
|
2018-03-07 05:39:41 -05:00
|
|
|
access.can_update_branch?(target_branch)
|
2016-07-18 04:16:56 -04:00
|
|
|
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?
|
2017-11-30 13:00:09 -05:00
|
|
|
return true unless head_pipeline
|
2016-06-07 07:01:34 -04:00
|
|
|
|
2017-12-01 08:26:51 -05:00
|
|
|
actual_head_pipeline&.success? || actual_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?
|
2017-12-12 22:44:59 -05:00
|
|
|
"git-merge"
|
2016-02-29 15:13:00 -05:00
|
|
|
elsif closed?
|
2017-12-12 22:44:59 -05:00
|
|
|
"close"
|
2016-02-29 15:13:00 -05:00
|
|
|
else
|
2017-12-12 22:44:59 -05:00
|
|
|
"issue-open-m"
|
2016-02-29 15:13:00 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-11-01 10:31:35 -04:00
|
|
|
def fetch_ref!
|
|
|
|
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
|
2015-08-11 08:33:31 -04:00
|
|
|
end
|
|
|
|
|
2015-09-21 10:25:59 -04:00
|
|
|
def ref_path
|
2017-08-25 11:00:06 -04:00
|
|
|
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
|
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
|
2017-08-10 15:16:43 -04:00
|
|
|
unlock_mr
|
2015-08-11 08:33:31 -04:00
|
|
|
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
|
|
|
|
2017-11-28 08:43:31 -05:00
|
|
|
target_project.repository
|
|
|
|
.count_commits_between(source_branch_sha, target_branch_sha)
|
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
|
2017-12-06 08:32:12 -05:00
|
|
|
.where(sha: all_commit_shas, ref: source_branch)
|
2016-10-17 09:31:21 -04:00
|
|
|
.order(id: :desc)
|
2016-09-20 05:36:21 -04:00
|
|
|
end
|
2016-09-19 11:51:27 -04:00
|
|
|
|
2017-11-22 09:48:09 -05:00
|
|
|
def all_commits
|
2017-11-21 11:58:08 -05:00
|
|
|
# MySQL doesn't support LIMIT in a subquery.
|
2017-12-01 14:08:30 -05:00
|
|
|
diffs_relation = if Gitlab::Database.postgresql?
|
|
|
|
merge_request_diffs.recent
|
|
|
|
else
|
|
|
|
merge_request_diffs
|
|
|
|
end
|
2017-07-07 07:56:09 -04:00
|
|
|
|
2017-11-21 11:58:08 -05:00
|
|
|
MergeRequestDiffCommit
|
|
|
|
.where(merge_request_diff: diffs_relation)
|
|
|
|
.limit(10_000)
|
2017-11-22 09:48:09 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Note that this could also return SHA from now dangling commits
|
|
|
|
#
|
|
|
|
def all_commit_shas
|
|
|
|
@all_commit_shas ||= begin
|
|
|
|
return commit_shas unless persisted?
|
2017-12-01 14:08:30 -05:00
|
|
|
|
2017-11-22 09:48:09 -05:00
|
|
|
all_commits.pluck(:sha).uniq
|
|
|
|
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)
|
2018-01-11 11:20:13 -05:00
|
|
|
return false unless merge_commit
|
|
|
|
|
|
|
|
merged_at = metrics&.merged_at
|
|
|
|
notes_association = notes_with_associations
|
|
|
|
|
|
|
|
if merged_at
|
2018-01-29 05:49:14 -05:00
|
|
|
# It is not guaranteed that Note#created_at will be strictly later than
|
|
|
|
# MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this
|
|
|
|
# comparison, as will a HA environment if clocks are not *precisely*
|
|
|
|
# synchronized. Add a minute's leeway to compensate for both possibilities
|
|
|
|
cutoff = merged_at - 1.minute
|
|
|
|
|
2018-01-24 10:06:50 -05:00
|
|
|
notes_association = notes_association.where('created_at >= ?', cutoff)
|
2018-01-11 11:20:13 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
!merge_commit.has_been_reverted?(current_user, notes_association)
|
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
|
2017-09-05 11:49:05 -04:00
|
|
|
|
|
|
|
if project.resolve_outdated_diff_discussions?
|
|
|
|
MergeRequests::ResolvedDiscussionNotificationService
|
|
|
|
.new(project, current_user)
|
|
|
|
.execute(self)
|
|
|
|
end
|
2016-06-20 13:22:39 -04:00
|
|
|
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)
|
2017-12-01 08:26:51 -05:00
|
|
|
return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
|
2016-11-24 09:05:15 -05:00
|
|
|
return false if last_diff_sha != diff_head_sha
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
2017-08-10 10:20:53 -04:00
|
|
|
|
2017-08-17 11:21:25 -04:00
|
|
|
def update_project_counter_caches
|
|
|
|
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
|
|
|
|
end
|
|
|
|
|
2017-08-29 09:46:40 -04:00
|
|
|
def first_contribution?
|
2017-08-15 09:21:27 -04:00
|
|
|
return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
|
2017-08-29 09:46:40 -04:00
|
|
|
|
2017-08-15 09:21:27 -04:00
|
|
|
project.merge_requests.merged.where(author_id: author_id).empty?
|
|
|
|
end
|
2018-02-23 12:30:37 -05:00
|
|
|
|
|
|
|
def allow_maintainer_to_push
|
|
|
|
maintainer_push_possible? && super
|
|
|
|
end
|
|
|
|
|
|
|
|
alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push
|
|
|
|
|
|
|
|
def maintainer_push_possible?
|
|
|
|
source_project.present? && for_fork? &&
|
|
|
|
target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
|
|
|
|
source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
|
|
|
|
!ProtectedBranch.protected?(source_project, source_branch)
|
|
|
|
end
|
|
|
|
|
|
|
|
def can_allow_maintainer_to_push?(user)
|
|
|
|
maintainer_push_possible? &&
|
|
|
|
Ability.allowed?(user, :push_code, source_project)
|
|
|
|
end
|
2011-11-28 02:39:43 -05:00
|
|
|
end
|