merge_request.rb 14.8 KB
Newer Older
1 2 3 4
# == Schema Information
#
# Table name: merge_requests
#
Stan Hu's avatar
Stan Hu committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
#  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)
#  locked_at                 :datetime
#  updated_by_id             :integer
#  merge_error               :string(255)
#  merge_params              :text
#  merge_when_build_succeeds :boolean          default(FALSE), not null
#  merge_user_id             :integer
27
#  merge_commit_sha          :string
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
28
#
29

30
require Rails.root.join("app/models/commit")
31
require Rails.root.join("lib/static_model")
32

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
33
class MergeRequest < ActiveRecord::Base
34
  include InternalId
35 36
  include Issuable
  include Referable
37
  include Sortable
38
  include Taskable
39

40 41
  belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
  belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
42
  belongs_to :merge_user, class_name: "User"
43

44
  has_one :merge_request_diff, dependent: :destroy
45

46 47
  serialize :merge_params, Hash

48
  after_create :create_merge_request_diff
49
  after_update :update_merge_request_diff
50

51
  delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
52

53 54 55 56
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

57 58
  # Temporary fields to store compare vars
  # when creating new merge request
59
  attr_accessor :can_be_created, :compare_commits, :compare
60

Andrew8xx8's avatar
Andrew8xx8 committed
61
  state_machine :state, initial: :opened do
62 63 64 65
    event :close do
      transition [:reopened, :opened] => :closed
    end

66
    event :mark_as_merged do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
67
      transition [:reopened, :opened, :locked] => :merged
68 69 70
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
71
      transition closed: :reopened
72 73
    end

74
    event :lock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
75 76 77
      transition [:reopened, :opened] => :locked
    end

78
    event :unlock_mr do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
79 80 81
      transition locked: :reopened
    end

82 83 84 85 86
    after_transition any => :locked do |merge_request, transition|
      merge_request.locked_at = Time.now
      merge_request.save
    end

87
    after_transition locked: (any - :locked) do |merge_request, transition|
88 89 90 91
      merge_request.locked_at = nil
      merge_request.save
    end

92 93 94 95
    state :opened
    state :reopened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
96
    state :locked
97 98
  end

99 100 101 102 103 104
  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
105
      transition [:unchecked, :cannot_be_merged] => :can_be_merged
106 107 108
    end

    event :mark_as_unmergeable do
109
      transition [:unchecked, :can_be_merged] => :cannot_be_merged
110 111
    end

112
    state :unchecked
113 114
    state :can_be_merged
    state :cannot_be_merged
115 116 117 118 119 120 121 122 123

    around_transition do |merge_request, transition, block|
      merge_request.record_timestamps = false
      begin
        block.call
      ensure
        merge_request.record_timestamps = true
      end
    end
124
  end
125

126
  validates :source_project, presence: true, unless: :allow_broken
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
127
  validates :source_branch, presence: true
128
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
129
  validates :target_branch, presence: true
130
  validates :merge_user, presence: true, if: :merge_when_build_succeeds?
131
  validate :validate_branches
132
  validate :validate_fork
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
133

134
  scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
135
  scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
136
  scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
137
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
138
  scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
139
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
140 141
  scope :merged, -> { with_state(:merged) }
  scope :closed_and_merged, -> { with_states(:closed, :merged) }
142

143 144 145
  scope :join_project, -> { joins(:target_project) }
  scope :references_project, -> { references(:target_project) }

146 147 148 149
  def self.reference_prefix
    '!'
  end

150 151 152 153 154
  # Pattern used to extract `!123` merge request references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
    %r{
155
      (#{Project.reference_pattern})?
156 157 158 159
      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
    }x
  end

160 161 162 163
  def self.link_reference_pattern
    super("merge_requests", /(?<merge_request>\d+)/)
  end

164 165 166 167 168 169 170 171 172 173
  def to_reference(from_project = nil)
    reference = "#{self.class.reference_prefix}#{iid}"

    if cross_project_reference?(from_project)
      reference = project.to_reference + reference
    end

    reference
  end

174 175
  def last_commit
    merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
176
  end
177 178 179

  def first_commit
    merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
180
  end
181

182 183 184 185
  def diff_size
    merge_request_diff.size
  end

186 187 188
  def diff_base_commit
    if merge_request_diff
      merge_request_diff.base_commit
Douwe Maan's avatar
Douwe Maan committed
189
    elsif source_sha
190
      self.target_project.merge_base_commit(self.source_sha, self.target_branch)
191 192 193
    end
  end

194 195 196 197
  def last_commit_short_sha
    last_commit.short_id
  end

198
  def validate_branches
199
    if target_project == source_project && target_branch == source_branch
200
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
201
    end
202

203
    if opened? || reopened?
Izaak Alpert's avatar
Izaak Alpert committed
204
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
205 206
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
      if similar_mrs.any?
207
        errors.add :validate_branches,
Gabriel Mazetto's avatar
Gabriel Mazetto committed
208
                   "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
209
      end
210
    end
211 212
  end

213
  def validate_fork
214 215 216
    return true unless target_project && source_project

    if target_project == source_project
217 218 219 220 221 222 223
      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
224 225
        errors.add :validate_fork,
                   'Source project is not a fork of target project'
226 227 228 229
      end
    end
  end

230 231 232 233 234 235
  def update_merge_request_diff
    if source_branch_changed? || target_branch_changed?
      reload_code
    end
  end

236
  def reload_code
237
    if merge_request_diff && open?
238 239
      merge_request_diff.reload_content
    end
240 241
  end

242
  def check_if_can_be_merged
243 244
    return unless unchecked?

245
    can_be_merged =
246
      !broken? && project.repository.can_be_merged?(source_sha, target_branch)
247 248

    if can_be_merged
249 250 251 252
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
253 254
  end

255
  def merge_event
256
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
257 258
  end

259
  def closed_event
260
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
261 262
  end

263
  def work_in_progress?
264
    !!(title =~ /\A\[?WIP(\]|:| )/i)
265 266
  end

267
  def mergeable?
268
    return false unless open? && !work_in_progress? && !broken?
269 270 271 272

    check_if_can_be_merged

    can_be_merged?
273 274
  end

275
  def gitlab_merge_status
276 277 278 279 280 281 282
    if work_in_progress?
      "work_in_progress"
    else
      merge_status_name
    end
  end

283 284
  def can_cancel_merge_when_build_succeeds?(current_user)
    can_be_merged_by?(current_user) || self.author == current_user
285 286
  end

287 288 289
  def can_remove_source_branch?(current_user)
    !source_project.protected_branch?(source_branch) &&
      !source_project.root_ref?(source_branch) &&
290 291
      Ability.abilities.allowed?(current_user, :push_code, source_project) &&
      last_commit == source_project.commit(source_branch)
292 293
  end

294
  def mr_and_commit_notes
295 296 297 298
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
    commit_ids = commits.last(commits_for_notes_limit).map(&:id)

299 300
    Note.where(
      "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
301
      "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
302
      mr_id: id,
303 304 305
      commit_ids: commit_ids,
      target_project_id: target_project_id,
      source_project_id: source_project_id
306
    )
307
  end
308

309 310 311
  # Returns the raw diff for this merge request
  #
  # see "git diff"
312
  def to_diff(current_user)
313
    target_project.repository.diff_text(target_branch, source_sha)
314 315 316 317 318
  end

  # Returns the commit as a series of email patches.
  #
  # see "git format-patch"
319
  def to_patch(current_user)
320
    target_project.repository.format_patch(target_branch, source_sha)
321
  end
322

Kirill Zaitsev's avatar
Kirill Zaitsev committed
323 324 325 326
  def hook_attrs
    attrs = {
      source: source_project.hook_attrs,
      target: target_project.hook_attrs,
327 328
      last_commit: nil,
      work_in_progress: work_in_progress?
Kirill Zaitsev's avatar
Kirill Zaitsev committed
329 330
    }

Valery Sizov's avatar
Valery Sizov committed
331
    if last_commit
332
      attrs.merge!(last_commit: last_commit.hook_attrs)
Kirill Zaitsev's avatar
Kirill Zaitsev committed
333 334 335 336 337
    end

    attributes.merge!(attrs)
  end

338 339 340 341
  def for_fork?
    target_project != source_project
  end

342 343 344 345
  def project
    target_project
  end

346 347 348 349
  def closes_issue?(issue)
    closes_issues.include?(issue)
  end

350
  # Return the set of issues that will be closed if this merge request is accepted.
351
  def closes_issues(current_user = self.author)
352
    if target_branch == project.default_branch
353 354 355 356
      messages = commits.map(&:safe_message) << description

      Gitlab::ClosingIssueExtractor.new(project, current_user).
        closed_by_message(messages.join("\n"))
357 358 359 360 361
    else
      []
    end
  end

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
  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

378 379 380 381 382 383 384 385
  def source_project_namespace
    if source_project && source_project.namespace
      source_project.namespace.path
    else
      "(removed)"
    end
  end

386 387 388 389 390 391 392 393
  def target_project_namespace
    if target_project && target_project.namespace
      target_project.namespace.path
    else
      "(removed)"
    end
  end

394 395 396 397 398 399 400 401 402 403 404 405
  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
406 407 408 409 410 411 412 413 414
  # 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
415
    Event.reset_event_cache_for(self)
Drew Blessing's avatar
Drew Blessing committed
416 417
  end

418 419 420
  def merge_commit_message
    message = "Merge branch '#{source_branch}' into '#{target_branch}'"
    message << "\n\n"
421
    message << title.to_s
422
    message << "\n\n"
423
    message << description.to_s
424 425
    message << "\n\n"
    message << "See merge request !#{iid}"
426
    message
427
  end
428

429 430
  def reset_merge_when_build_succeeds
    return unless merge_when_build_succeeds?
431

432 433 434 435 436 437 438
    self.merge_when_build_succeeds = false
    self.merge_user = nil
    self.merge_params = nil

    self.save
  end

439
  # Return array of possible target branches
Steven Burgart's avatar
Steven Burgart committed
440
  # depends on target project of MR
441 442 443 444 445 446 447 448 449
  def target_branches
    if target_project.nil?
      []
    else
      target_project.repository.branch_names
    end
  end

  # Return array of possible source branches
Steven Burgart's avatar
Steven Burgart committed
450
  # depends on source project of MR
451 452 453 454 455 456 457
  def source_branches
    if source_project.nil?
      []
    else
      source_project.repository.branch_names
    end
  end
458 459

  def locked_long_ago?
Ben Bodenmiller's avatar
Ben Bodenmiller committed
460 461 462
    return false unless locked?

    locked_at.nil? || locked_at < (Time.now - 1.day)
463
  end
464 465 466 467 468 469 470 471

  def has_ci?
    source_project.ci_service && commits.any?
  end

  def branch_missing?
    !source_branch_exists? || !target_branch_exists?
  end
472

473 474 475 476
  def broken?
    self.commits.blank? || branch_missing? || cannot_be_merged?
  end

477 478 479
  def can_be_merged_by?(user)
    ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
  end
480 481 482 483 484 485 486 487 488 489

  def state_human_name
    if merged?
      "Merged"
    elsif closed?
      "Closed"
    else
      "Open"
    end
  end
490

491 492 493 494 495 496 497 498 499 500
  def state_icon_name
    if merged?
      "check"
    elsif closed?
      "times"
    else
      "circle-o"
    end
  end

501
  def target_sha
502
    @target_sha ||= target_project.repository.commit(target_branch).sha
503 504 505
  end

  def source_sha
Douwe Maan's avatar
Douwe Maan committed
506
    last_commit.try(:sha)
507 508 509 510 511 512
  end

  def fetch_ref
    target_project.repository.fetch_ref(
      source_project.repository.path_to_repo,
      "refs/heads/#{source_branch}",
513
      ref_path
514 515 516
    )
  end

517 518 519 520 521 522 523 524 525 526 527 528
  def ref_path
    "refs/merge-requests/#{iid}/head"
  end

  def ref_is_fetched?
    File.exists?(File.join(project.repository.path_to_repo, ref_path))
  end

  def ensure_ref_fetched
    fetch_ref unless ref_is_fetched?
  end

529 530 531 532 533 534 535 536
  def in_locked_state
    begin
      lock_mr
      yield
    ensure
      unlock_mr if locked?
    end
  end
537

538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
  def diverged_commits_count
    cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")

    if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha
      cache = {
        source_sha: source_sha,
        target_sha: target_sha,
        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
    Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
  end

  def diverged_from_target_branch?
    diverged_commits_count > 0
  end

561
  def ci_commit
562
    @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
563
  end
564

Douwe Maan's avatar
Douwe Maan committed
565
  def diff_refs
566 567 568
    return nil unless diff_base_commit

    [diff_base_commit, last_commit]
569
  end
570 571 572 573 574

  def merge_commit
    @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
  end

575 576
  def can_be_reverted?(current_user = nil)
    merge_commit && !merge_commit.has_been_reverted?(current_user, self)
577
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
578
end