merge_request.rb 9.5 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1 2 3 4
# == Schema Information
#
# Table name: merge_requests
#
D
Dmitriy Zaporozhets 已提交
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
D
Dmitriy Zaporozhets 已提交
21
#  description       :text
D
Dmitriy Zaporozhets 已提交
22
#
D
Dmitriy Zaporozhets 已提交
23

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

D
Dmitriy Zaporozhets 已提交
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"
I
Izaak Alpert 已提交
33

34
  attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :author_id_of_changes, :state_event, :description
35

A
Andrey Kumanyaev 已提交
36 37
  attr_accessor :should_remove_source_branch

D
Dmitriy Zaporozhets 已提交
38 39 40 41
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

A
Andrew8xx8 已提交
42
  state_machine :state, initial: :opened do
A
Andrew8xx8 已提交
43 44 45 46 47 48 49 50 51
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :merge do
      transition [:reopened, :opened] => :merged
    end

    event :reopen do
A
Andrew8xx8 已提交
52
      transition closed: :reopened
A
Andrew8xx8 已提交
53 54 55 56 57 58 59 60 61 62 63
    end

    state :opened

    state :reopened

    state :closed

    state :merged
  end

64 65 66 67 68 69 70 71 72 73 74 75 76
  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

77
    state :unchecked
78

79 80 81 82
    state :can_be_merged

    state :cannot_be_merged
  end
83

84 85 86
  serialize :st_commits
  serialize :st_diffs

D
Dmitriy Zaporozhets 已提交
87
  validates :source_project, presence: true, unless: :allow_broken
A
Andrey Kumanyaev 已提交
88
  validates :source_branch, presence: true
I
Izaak Alpert 已提交
89
  validates :target_project, presence: true
A
Andrey Kumanyaev 已提交
90
  validates :target_branch, presence: true
I
Izaak Alpert 已提交
91
  validate :validate_branches
D
Dmitriy Zaporozhets 已提交
92

93 94
  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) }
I
Izaak Alpert 已提交
95 96
  scope :opened, -> { with_state(:opened) }
  scope :closed, -> { with_state(:closed) }
A
Andrew8xx8 已提交
97
  scope :merged, -> { with_state(:merged) }
I
Izaak Alpert 已提交
98
  scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
A
Andrew8xx8 已提交
99
  scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
100
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
I
Izaak Alpert 已提交
101
  scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
102
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
103 104 105 106
  # Closed scope for merge request should return
  # both merged and closed mr's
  scope :closed, -> { with_states(:closed, :merged) }

107
  def validate_branches
I
Izaak Alpert 已提交
108 109
    if target_project==source_project && target_branch == source_branch
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
110
    end
111

112
    if opened? || reopened?
I
Izaak Alpert 已提交
113
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
114
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
115

116
      if similar_mrs.any?
I
Izaak Alpert 已提交
117
        errors.add :base, "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
118
      end
119
    end
120 121
  end

122 123 124 125 126
  def reload_code
    self.reloaded_commits
    self.reloaded_diffs
  end

127
  def check_if_can_be_merged
128 129 130 131 132
    if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
133 134
  end

D
Dmitriy Zaporozhets 已提交
135
  def diffs
136
    @diffs ||= (load_diffs(st_diffs) || [])
137 138 139
  end

  def reloaded_diffs
A
Andrew8xx8 已提交
140
    if opened? && unmerged_diffs.any?
141
      self.st_diffs = dump_diffs(unmerged_diffs)
142
      self.save
143
    end
144 145 146
  end

  def broken_diffs?
147
    diffs == broken_diffs
148 149
  rescue
    true
150 151 152 153
  end

  def valid_diffs?
    !broken_diffs?
154 155 156
  end

  def unmerged_diffs
157 158 159
    diffs = if for_fork?
              Gitlab::Satellite::MergeAction.new(author, self).diffs_between_satellite
            else
160
              Gitlab::Git::Diff.between(target_project.repository, source_branch, target_branch)
161 162
            end

163
    diffs ||= []
I
Izaak Alpert 已提交
164
    diffs
D
Dmitriy Zaporozhets 已提交
165 166 167
  end

  def last_commit
168
    commits.first
D
Dmitriy Zaporozhets 已提交
169
  end
170

D
Dmitriy Zaporozhets 已提交
171
  def merge_event
I
Izaak Alpert 已提交
172
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
D
Dmitriy Zaporozhets 已提交
173 174
  end

175
  def closed_event
I
Izaak Alpert 已提交
176
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
177 178
  end

179
  def commits
180
    load_commits(st_commits || [])
181 182 183
  end

  def probably_merged?
184
    unmerged_commits.empty? &&
185
      commits.any? && opened?
186 187
  end

188
  def reloaded_commits
A
Andrew8xx8 已提交
189
    if opened? && unmerged_commits.any?
190
      self.st_commits = dump_commits(unmerged_commits)
191
      save
I
Izaak Alpert 已提交
192

193 194 195 196 197
    end
    commits
  end

  def unmerged_commits
198 199 200 201 202
    if for_fork?
      commits = Gitlab::Satellite::MergeAction.new(self.author, self).commits_between
    else
      commits = target_project.repository.commits_between(self.target_branch, self.source_branch)
    end
203

I
Izaak Alpert 已提交
204 205
    if commits.present?
      commits = Commit.decorate(commits).
206 207
      sort_by(&:created_at).
      reverse
I
Izaak Alpert 已提交
208 209
    end
    commits
210
  end
211 212

  def merge!(user_id)
213
    self.author_id_of_changes = user_id
A
Andrew8xx8 已提交
214
    self.merge
215
  end
216

217 218
  def automerge!(current_user, merge_commit_message = nil)
    if Gitlab::Satellite::MergeAction.new(current_user, self).merge!(merge_commit_message) && self.unmerged_commits.empty?
219 220 221
      self.merge!(current_user.id)
      true
    end
Z
Zevs 已提交
222
  rescue
223
    mark_as_unmergeable
224 225
    false
  end
R
randx 已提交
226

227 228
  def mr_and_commit_notes
    commit_ids = commits.map(&:id)
229 230 231 232 233
    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
    )
234
  end
235

236 237 238
  # Returns the raw diff for this merge request
  #
  # see "git diff"
I
Izaak Alpert 已提交
239 240
  def to_diff(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).diff_in_satellite
241 242 243 244 245
  end

  # Returns the commit as a series of email patches.
  #
  # see "git format-patch"
I
Izaak Alpert 已提交
246 247
  def to_patch(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).format_patch
248
  end
249 250 251 252

  def last_commit_short_sha
    @last_commit_short_sha ||= last_commit.sha[0..10]
  end
253

I
Izaak Alpert 已提交
254 255 256 257 258 259 260 261
  def for_fork?
    target_project != source_project
  end

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

262 263 264 265
  def project
    target_project
  end

266 267 268
  # Return the set of issues that will be closed if this merge request is accepted.
  def closes_issues
    if target_branch == project.default_branch
269
      commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id)
270 271 272 273 274 275 276 277 278 279
    else
      []
    end
  end

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

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
  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

D
Drew Blessing 已提交
308 309 310 311 312 313 314 315 316 317 318 319 320 321
  # 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

322 323 324 325 326 327 328 329
  def merge_commit_message
    message = "Merge branch '#{source_branch}' into '#{target_branch}'"
    message << "\n\n"
    message << title
    message << "\n\n"
    message << description
  end

330 331 332 333 334 335 336 337 338
  private

  def dump_commits(commits)
    commits.map(&:to_hash)
  end

  def load_commits(array)
    array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash)) }
  end
339 340

  def dump_diffs(diffs)
341
    if diffs == broken_diffs
342
      broken_diffs
343
    elsif diffs.respond_to?(:map)
344 345 346 347 348 349 350
      diffs.map(&:to_hash)
    end
  end

  def load_diffs(raw)
    if raw == broken_diffs
      broken_diffs
351
    elsif raw.respond_to?(:map)
352 353
      raw.map { |hash| Gitlab::Git::Diff.new(hash) }
    end
354 355
  end

356 357
  def broken_diffs
    [Gitlab::Git::Diff::BROKEN_DIFF]
358
  end
D
Dmitriy Zaporozhets 已提交
359
end