Class: MergeRequest

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
InternalId, Issuable, Referable, Sortable, Taskable
Defined in:
app/models/merge_request.rb

Overview

Schema Information

Table name: merge_requests

id                        :integer          not null, primary key
target_branch             :string           not null
source_branch             :string           not null
source_project_id         :integer          not null
author_id                 :integer
assignee_id               :integer
title                     :string
created_at                :datetime
updated_at                :datetime
milestone_id              :integer
state                     :string
merge_status              :string
target_project_id         :integer          not null
iid                       :integer
description               :text
position                  :integer          default(0)
locked_at                 :datetime
updated_by_id             :integer
merge_error               :string
merge_params              :text
merge_when_build_succeeds :boolean          default(FALSE), not null
merge_user_id             :integer
merge_commit_sha          :string
deleted_at                :datetime

Constant Summary collapse

WIP_REGEX =
/\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze

Constants included from Taskable

Taskable::COMPLETED, Taskable::INCOMPLETE, Taskable::ITEM_PATTERN

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Taskable

get_tasks, get_updated_tasks, #task_list_items, #task_status, #tasks, #tasks?

Methods included from Referable

#reference_link_text

Methods included from Issuable

#add_labels_by_names, #can_move?, #card_attributes, #downvotes, #is_assigned?, #is_being_reassigned?, #label_names, #new?, #notes_with_associations, #open?, #remove_labels, #subscribed_without_subscriptions?, #to_ability_name, #to_hook_data, #today?, #updated_tasks, #upvotes

Methods included from StripAttribute

#strip_attributes

Methods included from Subscribable

#subscribed?, #subscribed_without_subscriptions?, #subscribers, #toggle_subscription, #unsubscribe

Methods included from Mentionable

#all_references, #create_cross_references!, #create_new_cross_references!, #gfm_reference, #local_reference, #mentioned_users, #referenced_mentionables

Methods included from Participable

#participants

Methods included from InternalId

#set_iid, #to_param

Instance Attribute Details

#allow_brokenObject

When this attribute is true some MR validation is ignored It allows us to close or modify broken merge requests


53
54
55
# File 'app/models/merge_request.rb', line 53

def allow_broken
  @allow_broken
end

#can_be_createdObject

Temporary fields to store compare vars when creating new merge request


57
58
59
# File 'app/models/merge_request.rb', line 57

def can_be_created
  @can_be_created
end

#compareObject

Temporary fields to store compare vars when creating new merge request


57
58
59
# File 'app/models/merge_request.rb', line 57

def compare
  @compare
end

#compare_commitsObject

Temporary fields to store compare vars when creating new merge request


57
58
59
# File 'app/models/merge_request.rb', line 57

def compare_commits
  @compare_commits
end

Class Method Details

.in_projects(relation) ⇒ Object

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.


171
172
173
174
175
176
177
# File 'app/models/merge_request.rb', line 171

def self.in_projects(relation)
  source = where(source_project_id: relation).select(:id)
  target = where(target_project_id: relation).select(:id)
  union  = Gitlab::SQL::Union.new([source, target])

  where("merge_requests.id IN (#{union.to_sql})")
end

157
158
159
# File 'app/models/merge_request.rb', line 157

def self.link_reference_pattern
  @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end

.reference_patternObject

Pattern used to extract `!123` merge request references from text

This pattern supports cross-project references.


150
151
152
153
154
155
# File 'app/models/merge_request.rb', line 150

def self.reference_pattern
  @reference_pattern ||= %r{
    (#{Project.reference_pattern})?
    #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
  }x
end

.reference_prefixObject


143
144
145
# File 'app/models/merge_request.rb', line 143

def self.reference_prefix
  '!'
end

Instance Method Details

#branch_missing?Boolean

Returns:

  • (Boolean)

490
491
492
# File 'app/models/merge_request.rb', line 490

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

#broken?Boolean

Returns:

  • (Boolean)

494
495
496
# File 'app/models/merge_request.rb', line 494

def broken?
  self.commits.blank? || branch_missing? || cannot_be_merged?
end

#can_be_cherry_picked?Boolean

Returns:

  • (Boolean)

607
608
609
# File 'app/models/merge_request.rb', line 607

def can_be_cherry_picked?
  merge_commit
end

#can_be_merged_by?(user) ⇒ Boolean

Returns:

  • (Boolean)

498
499
500
# File 'app/models/merge_request.rb', line 498

def can_be_merged_by?(user)
  ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
end

#can_be_reverted?(current_user = nil) ⇒ Boolean

Returns:

  • (Boolean)

603
604
605
# File 'app/models/merge_request.rb', line 603

def can_be_reverted?(current_user = nil)
  merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end

#can_cancel_merge_when_build_succeeds?(current_user) ⇒ Boolean

Returns:

  • (Boolean)

304
305
306
# File 'app/models/merge_request.rb', line 304

def can_cancel_merge_when_build_succeeds?(current_user)
  can_be_merged_by?(current_user) || self.author == current_user
end

#can_remove_source_branch?(current_user) ⇒ Boolean

Returns:

  • (Boolean)

308
309
310
311
312
313
# File 'app/models/merge_request.rb', line 308

def can_remove_source_branch?(current_user)
  !source_project.protected_branch?(source_branch) &&
    !source_project.root_ref?(source_branch) &&
    Ability.abilities.allowed?(current_user, :push_code, source_project) &&
    last_commit == source_project.commit(source_branch)
end

#check_if_can_be_mergedObject


257
258
259
260
261
262
263
264
265
266
267
268
# File 'app/models/merge_request.rb', line 257

def check_if_can_be_merged
  return unless unchecked?

  can_be_merged =
    !broken? && project.repository.can_be_merged?(source_sha, target_branch)

  if can_be_merged
    mark_as_mergeable
  else
    mark_as_unmergeable
  end
end

#ci_commitObject


589
590
591
# File 'app/models/merge_request.rb', line 589

def ci_commit
  @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project
end

#closed_eventObject


274
275
276
# File 'app/models/merge_request.rb', line 274

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

#closes_issue?(issue) ⇒ Boolean

Returns:

  • (Boolean)

367
368
369
# File 'app/models/merge_request.rb', line 367

def closes_issue?(issue)
  closes_issues.include?(issue)
end

#closes_issues(current_user = self.author) ⇒ Object

Return the set of issues that will be closed if this merge request is accepted.


372
373
374
375
376
377
378
379
380
381
# File 'app/models/merge_request.rb', line 372

def closes_issues(current_user = self.author)
  if target_branch == project.default_branch
    messages = commits.map(&:safe_message) << description

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

#diff_base_commitObject


201
202
203
204
205
206
207
# File 'app/models/merge_request.rb', line 201

def diff_base_commit
  if merge_request_diff
    merge_request_diff.base_commit
  elsif source_sha
    self.target_project.merge_base_commit(self.source_sha, self.target_branch)
  end
end

#diff_refsObject


593
594
595
596
597
# File 'app/models/merge_request.rb', line 593

def diff_refs
  return nil unless diff_base_commit

  [diff_base_commit, last_commit]
end

#diff_sizeObject


197
198
199
# File 'app/models/merge_request.rb', line 197

def diff_size
  merge_request_diff.size
end

#diverged_commits_countObject


563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'app/models/merge_request.rb', line 563

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

#diverged_from_target_branch?Boolean

Returns:

  • (Boolean)

585
586
587
# File 'app/models/merge_request.rb', line 585

def diverged_from_target_branch?
  diverged_commits_count > 0
end

#ensure_ref_fetchedObject


550
551
552
# File 'app/models/merge_request.rb', line 550

def ensure_ref_fetched
  fetch_ref unless ref_is_fetched?
end

#fetch_refObject


534
535
536
537
538
539
540
# File 'app/models/merge_request.rb', line 534

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

#first_commitObject


193
194
195
# File 'app/models/merge_request.rb', line 193

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

#for_fork?Boolean

Returns:

  • (Boolean)

359
360
361
# File 'app/models/merge_request.rb', line 359

def for_fork?
  target_project != source_project
end

#gitlab_merge_statusObject


296
297
298
299
300
301
302
# File 'app/models/merge_request.rb', line 296

def gitlab_merge_status
  if work_in_progress?
    "work_in_progress"
  else
    merge_status_name
  end
end

#has_ci?Boolean

Returns:

  • (Boolean)

486
487
488
# File 'app/models/merge_request.rb', line 486

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

#hook_attrsObject


344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'app/models/merge_request.rb', line 344

def hook_attrs
  attrs = {
    source: source_project.try(:hook_attrs),
    target: target_project.hook_attrs,
    last_commit: nil,
    work_in_progress: work_in_progress?
  }

  if last_commit
    attrs.merge!(last_commit: last_commit.hook_attrs)
  end

  attributes.merge!(attrs)
end

#in_locked_stateObject


554
555
556
557
558
559
560
561
# File 'app/models/merge_request.rb', line 554

def in_locked_state
  begin
    lock_mr
    yield
  ensure
    unlock_mr if locked?
  end
end

#last_commitObject


189
190
191
# File 'app/models/merge_request.rb', line 189

def last_commit
  merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
end

#last_commit_short_shaObject


209
210
211
# File 'app/models/merge_request.rb', line 209

def last_commit_short_sha
  last_commit.short_id
end

#locked_long_ago?Boolean

Returns:

  • (Boolean)

480
481
482
483
484
# File 'app/models/merge_request.rb', line 480

def locked_long_ago?
  return false unless locked?

  locked_at.nil? || locked_at < (Time.now - 1.day)
end

#merge_commitObject


599
600
601
# File 'app/models/merge_request.rb', line 599

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

#merge_commit_messageObject


439
440
441
442
443
444
445
446
447
448
# File 'app/models/merge_request.rb', line 439

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

#merge_eventObject


270
271
272
# File 'app/models/merge_request.rb', line 270

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

#mergeable?Boolean

Returns:

  • (Boolean)

288
289
290
291
292
293
294
# File 'app/models/merge_request.rb', line 288

def mergeable?
  return false unless open? && !work_in_progress? && !broken?

  check_if_can_be_merged

  can_be_merged?
end

#mr_and_commit_notesObject


315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'app/models/merge_request.rb', line 315

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)

  Note.where(
    "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
    "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
    mr_id: id,
    commit_ids: commit_ids,
    target_project_id: target_project_id,
    source_project_id: source_project_id
  )
end

#projectObject


363
364
365
# File 'app/models/merge_request.rb', line 363

def project
  target_project
end

#ref_is_fetched?Boolean

Returns:

  • (Boolean)

546
547
548
# File 'app/models/merge_request.rb', line 546

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

#ref_pathObject


542
543
544
# File 'app/models/merge_request.rb', line 542

def ref_path
  "refs/merge-requests/#{iid}/head"
end

#reload_codeObject


251
252
253
254
255
# File 'app/models/merge_request.rb', line 251

def reload_code
  if merge_request_diff && open?
    merge_request_diff.reload_content
  end
end

#reset_events_cacheObject

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.


435
436
437
# File 'app/models/merge_request.rb', line 435

def reset_events_cache
  Event.reset_event_cache_for(self)
end

#reset_merge_when_build_succeedsObject


450
451
452
453
454
455
456
457
458
# File 'app/models/merge_request.rb', line 450

def reset_merge_when_build_succeeds
  return unless merge_when_build_succeeds?

  self.merge_when_build_succeeds = false
  self.merge_user = nil
  self.merge_params = nil

  self.save
end

#source_branch_exists?Boolean

Returns:

  • (Boolean)

415
416
417
418
419
# File 'app/models/merge_request.rb', line 415

def source_branch_exists?
  return false unless self.source_project

  self.source_project.repository.branch_names.include?(self.source_branch)
end

#source_branchesObject

Return array of possible source branches depends on source project of MR


472
473
474
475
476
477
478
# File 'app/models/merge_request.rb', line 472

def source_branches
  if source_project.nil?
    []
  else
    source_project.repository.branch_names
  end
end

#source_project_namespaceObject


399
400
401
402
403
404
405
# File 'app/models/merge_request.rb', line 399

def source_project_namespace
  if source_project && source_project.namespace
    source_project.namespace.path
  else
    "(removed)"
  end
end

#source_project_pathObject


391
392
393
394
395
396
397
# File 'app/models/merge_request.rb', line 391

def source_project_path
  if source_project
    source_project.path_with_namespace
  else
    "(removed)"
  end
end

#source_shaObject


526
527
528
# File 'app/models/merge_request.rb', line 526

def source_sha
  last_commit.try(:sha) || source_tip.try(:sha)
end

#source_tipObject


530
531
532
# File 'app/models/merge_request.rb', line 530

def source_tip
  source_branch && source_project.repository.commit(source_branch)
end

#state_human_nameObject


502
503
504
505
506
507
508
509
510
# File 'app/models/merge_request.rb', line 502

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

#state_icon_nameObject


512
513
514
515
516
517
518
519
520
# File 'app/models/merge_request.rb', line 512

def state_icon_name
  if merged?
    "check"
  elsif closed?
    "times"
  else
    "circle-o"
  end
end

#target_branch_exists?Boolean

Returns:

  • (Boolean)

421
422
423
424
425
# File 'app/models/merge_request.rb', line 421

def target_branch_exists?
  return false unless self.target_project

  self.target_project.repository.branch_names.include?(self.target_branch)
end

#target_branchesObject

Return array of possible target branches depends on target project of MR


462
463
464
465
466
467
468
# File 'app/models/merge_request.rb', line 462

def target_branches
  if target_project.nil?
    []
  else
    target_project.repository.branch_names
  end
end

#target_project_namespaceObject


407
408
409
410
411
412
413
# File 'app/models/merge_request.rb', line 407

def target_project_namespace
  if target_project && target_project.namespace
    target_project.namespace.path
  else
    "(removed)"
  end
end

#target_project_pathObject


383
384
385
386
387
388
389
# File 'app/models/merge_request.rb', line 383

def target_project_path
  if target_project
    target_project.path_with_namespace
  else
    "(removed)"
  end
end

#target_shaObject


522
523
524
# File 'app/models/merge_request.rb', line 522

def target_sha
  @target_sha ||= target_project.repository.commit(target_branch).try(:sha)
end

#to_diffObject

Returns the raw diff for this merge request

see “git diff”


333
334
335
# File 'app/models/merge_request.rb', line 333

def to_diff
  target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end

#to_patchObject

Returns the commit as a series of email patches.

see “git format-patch”


340
341
342
# File 'app/models/merge_request.rb', line 340

def to_patch
  target_project.repository.format_patch(diff_base_commit.sha, source_sha)
end

#to_reference(from_project = nil) ⇒ Object


179
180
181
182
183
184
185
186
187
# File 'app/models/merge_request.rb', line 179

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

#update_merge_request_diffObject


245
246
247
248
249
# File 'app/models/merge_request.rb', line 245

def update_merge_request_diff
  if source_branch_changed? || target_branch_changed?
    reload_code
  end
end

#validate_branchesObject


213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'app/models/merge_request.rb', line 213

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.try(: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

#validate_forkObject


228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'app/models/merge_request.rb', line 228

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

#wipless_titleObject


284
285
286
# File 'app/models/merge_request.rb', line 284

def wipless_title
  self.title.sub(WIP_REGEX, "")
end

#work_in_progress?Boolean

Returns:

  • (Boolean)

280
281
282
# File 'app/models/merge_request.rb', line 280

def work_in_progress?
  !!(title =~ WIP_REGEX)
end