merge_request.rb 8.21 KB
Newer Older
1 2 3 4
# == Schema Information
#
# Table name: merge_requests
#
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#  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         not null
#  updated_at        :datetime         not null
#  st_commits        :text(2147483647)
#  st_diffs          :text(2147483647)
#  milestone_id      :integer
#  state             :string(255)
#  merge_status      :string(255)
#  target_project_id :integer          not null
#  iid               :integer
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
21
#  description       :text
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
22
#
23

24
require Rails.root.join("app/models/commit")
25
require Rails.root.join("lib/static_model")
26

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
27
class MergeRequest < ActiveRecord::Base
28
  include Issuable
29
  include InternalId
30

31 32
  belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
  belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
33

34
  has_one :merge_request_diff, dependent: :destroy
35
  after_create :create_merge_request_diff
36 37

  delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil
38

39
  attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :author_id_of_changes, :state_event, :description
40

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
41 42
  attr_accessor :should_remove_source_branch

43 44 45 46
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

Andrew8xx8's avatar
Andrew8xx8 committed
47
  state_machine :state, initial: :opened do
48 49 50 51 52
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :merge do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
53
      transition [:reopened, :opened, :locked] => :merged
54 55 56
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
57
      transition closed: :reopened
58 59
    end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
60 61 62 63 64 65 66 67
    event :lock do
      transition [:reopened, :opened] => :locked
    end

    event :unlock do
      transition locked: :reopened
    end

68 69 70 71
    state :opened
    state :reopened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
72
    state :locked
73 74
  end

75 76 77 78 79 80 81 82 83 84 85 86 87
  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

88
    state :unchecked
89 90 91
    state :can_be_merged
    state :cannot_be_merged
  end
92

93
  validates :source_project, presence: true, unless: :allow_broken
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
94
  validates :source_branch, presence: true
95
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
96
  validates :target_branch, presence: true
97
  validate :validate_branches
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
98

99 100
  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) }
101 102
  scope :opened, -> { with_state(:opened) }
  scope :closed, -> { with_state(:closed) }
103
  scope :merged, -> { with_state(:merged) }
104
  scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
105
  scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
106
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
107
  scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
108
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
109 110 111 112
  # Closed scope for merge request should return
  # both merged and closed mr's
  scope :closed, -> { with_states(:closed, :merged) }

113
  def validate_branches
114
    if target_project == source_project && target_branch == source_branch
115
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
116
    end
117

118
    if opened? || reopened?
Izaak Alpert's avatar
Izaak Alpert committed
119
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
120
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
121

122
      if similar_mrs.any?
Izaak Alpert's avatar
Izaak Alpert committed
123
        errors.add :base, "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
124
      end
125
    end
126 127
  end

128
  def reload_code
129 130 131
    if merge_request_diff && opened?
      merge_request_diff.reload_content
    end
132 133
  end

134
  def check_if_can_be_merged
135 136 137 138 139
    if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
140 141
  end

142
  def merge_event
143
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
144 145
  end

146
  def closed_event
147
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
148 149
  end

150
  def automerge!(current_user, commit_message = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
151
    MergeRequests::AutoMergeService.new.execute(self, current_user, commit_message)
152
  end
randx's avatar
randx committed
153

154
  def mr_and_commit_notes
155 156 157 158
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
    commit_ids = commits.last(commits_for_notes_limit).map(&:id)

159 160 161 162 163
    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
    )
164
  end
165

166 167 168
  # Returns the raw diff for this merge request
  #
  # see "git diff"
169 170
  def to_diff(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).diff_in_satellite
171 172 173 174 175
  end

  # Returns the commit as a series of email patches.
  #
  # see "git format-patch"
176 177
  def to_patch(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).format_patch
178
  end
179

180 181 182 183 184 185 186 187
  def for_fork?
    target_project != source_project
  end

  def disallow_source_branch_removal?
    (source_project.root_ref? source_branch) || for_fork?
  end

188 189 190 191
  def project
    target_project
  end

192 193 194
  # Return the set of issues that will be closed if this merge request is accepted.
  def closes_issues
    if target_branch == project.default_branch
195
      commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id)
196 197 198 199 200 201 202 203 204 205
    else
      []
    end
  end

  # Mentionable override.
  def gfm_reference
    "merge request !#{iid}"
  end

206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
  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_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

Drew Blessing's avatar
Drew Blessing committed
234 235 236 237 238 239 240 241 242 243 244 245 246 247
  # 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.where(target_id: self.id, target_type: 'MergeRequest').
        order('id DESC').limit(100).
        update_all(updated_at: Time.now)
  end

248 249 250
  def merge_commit_message
    message = "Merge branch '#{source_branch}' into '#{target_branch}'"
    message << "\n\n"
251
    message << title.to_s
252
    message << "\n\n"
253 254
    message << description.to_s
    message
255
  end
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

  # 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
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
276
end