gitlab-org--gitlab-foss/app/models/merge_request.rb
Vinnie Okada 9f0083a96c Add task lists to issues and merge requests
Make the Markdown parser recognize "[x]" or "[ ]" at the beginning of a
list item and turn it into a checkbox input.  Users who can modify the
issue or MR can toggle the checkboxes directly or edit the Markdown to
manage the tasks.  Task status is also displayed in the MR and issue
lists.
2014-10-05 22:15:27 -05:00

339 lines
9.5 KiB
Ruby

# == Schema Information
#
# Table name: merge_requests
#
# id :integer not null, primary key
# target_branch :string(255) not null
# source_branch :string(255) not null
# source_project_id :integer not null
# author_id :integer
# assignee_id :integer
# title :string(255)
# created_at :datetime
# updated_at :datetime
# milestone_id :integer
# state :string(255)
# merge_status :string(255)
# target_project_id :integer not null
# iid :integer
# description :text
# position :integer default(0)
#
require Rails.root.join("app/models/commit")
require Rails.root.join("lib/static_model")
class MergeRequest < ActiveRecord::Base
include Issuable
include Taskable
include InternalId
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
has_one :merge_request_diff, dependent: :destroy
after_create :create_merge_request_diff
after_update :update_merge_request_diff
delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil
attr_accessor :should_remove_source_branch
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
# Temporary fields to store compare vars
# when creating new merge request
attr_accessor :can_be_created, :compare_failed,
:compare_commits, :compare_diffs
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
end
event :merge do
transition [:reopened, :opened, :locked] => :merged
end
event :reopen do
transition closed: :reopened
end
event :lock_mr do
transition [:reopened, :opened] => :locked
end
event :unlock_mr do
transition locked: :reopened
end
state :opened
state :reopened
state :closed
state :merged
state :locked
end
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
transition unchecked: :can_be_merged
end
event :mark_as_unmergeable do
transition unchecked: :cannot_be_merged
end
state :unchecked
state :can_be_merged
state :cannot_be_merged
end
validates :source_project, presence: true, unless: :allow_broken
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
validate :validate_branches
validate :validate_fork
scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) }
scope :of_user_team, ->(team) { where("(source_project_id in (:team_project_ids) OR target_project_id in (:team_project_ids) AND assignee_id in (:team_member_ids))", team_project_ids: team.project_ids, team_member_ids: team.member_ids) }
scope :merged, -> { with_state(:merged) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
# Closed scope for merge request should return
# both merged and closed mr's
scope :closed, -> { with_states(:closed, :merged) }
scope :declined, -> { with_states(:closed) }
def validate_branches
if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target"
end
if opened? || reopened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
errors.add :validate_branches,
"Cannot Create: This merge request already exists: #{
similar_mrs.pluck(:title)
}"
end
end
end
def validate_fork
return true unless target_project && source_project
if target_project == source_project
true
else
# If source and target projects are different
# we should check if source project is actually a fork of target project
if source_project.forked_from?(target_project)
true
else
errors.add :validate_fork,
'Source project is not a fork of target project'
end
end
end
def update_merge_request_diff
if source_branch_changed? || target_branch_changed?
reload_code
mark_as_unchecked
end
end
def reload_code
if merge_request_diff && open?
merge_request_diff.reload_content
end
end
def check_if_can_be_merged
if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
mark_as_mergeable
else
mark_as_unmergeable
end
end
def merge_event
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
end
def closed_event
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
def automerge!(current_user, commit_message = nil)
MergeRequests::AutoMergeService.new.execute(self, current_user, commit_message)
end
def open?
opened? || reopened?
end
def mr_and_commit_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
project.notes.where(
"(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
mr_id: id,
commit_ids: commit_ids
)
end
# Returns the raw diff for this merge request
#
# see "git diff"
def to_diff(current_user)
Gitlab::Satellite::MergeAction.new(current_user, self).diff_in_satellite
end
# Returns the commit as a series of email patches.
#
# see "git format-patch"
def to_patch(current_user)
Gitlab::Satellite::MergeAction.new(current_user, self).format_patch
end
def hook_attrs
attrs = {
source: source_project.hook_attrs,
target: target_project.hook_attrs,
last_commit: nil
}
unless last_commit.nil?
attrs.merge!(last_commit: last_commit.hook_attrs(source_project))
end
attributes.merge!(attrs)
end
def for_fork?
target_project != source_project
end
def project
target_project
end
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues
if target_branch == project.default_branch
issues = commits.flat_map { |c| c.closes_issues(project) }
issues += Gitlab::ClosingIssueExtractor.closed_by_message_in_project(description, project)
issues.uniq.sort_by(&:id)
else
[]
end
end
# Mentionable override.
def gfm_reference
"merge request !#{iid}"
end
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
def source_project_namespace
if source_project && source_project.namespace
source_project.namespace.path
else
"(removed)"
end
end
def target_project_namespace
if target_project && target_project.namespace
target_project.namespace.path
else
"(removed)"
end
end
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
# Reset merge request events cache
#
# Since we do cache @event we need to reset cache in special cases:
# * when a merge request is updated
# Events cache stored like events/23-20130109142513.
# The cache key includes updated_at timestamp.
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.reset_event_cache_for(self)
end
def merge_commit_message
message = "Merge branch '#{source_branch}' into '#{target_branch}'"
message << "\n\n"
message << title.to_s
message << "\n\n"
message << description.to_s
message << "\n\n"
message << "See merge request !#{iid}"
message
end
# Return array of possible target branches
# dependes on target project of MR
def target_branches
if target_project.nil?
[]
else
target_project.repository.branch_names
end
end
# Return array of possible source branches
# dependes on source project of MR
def source_branches
if source_project.nil?
[]
else
source_project.repository.branch_names
end
end
end