1# frozen_string_literal: true 2 3class MergeRequest < ApplicationRecord 4 include AtomicInternalId 5 include IidRoutes 6 include Issuable 7 include Noteable 8 include Referable 9 include Presentable 10 include TimeTrackable 11 include ManualInverseAssociation 12 include EachBatch 13 include ThrottledTouch 14 include Gitlab::Utils::StrongMemoize 15 include LabelEventable 16 include ReactiveCaching 17 include FromUnion 18 include DeprecatedAssignee 19 include ShaAttribute 20 include IgnorableColumns 21 include MilestoneEventable 22 include StateEventable 23 include ApprovableBase 24 include IdInOrdered 25 include Todoable 26 27 extend ::Gitlab::Utils::Override 28 29 sha_attribute :squash_commit_sha 30 sha_attribute :merge_ref_sha 31 32 self.reactive_cache_key = ->(model) { [model.project.id, model.iid] } 33 self.reactive_cache_refresh_interval = 10.minutes 34 self.reactive_cache_lifetime = 10.minutes 35 self.reactive_cache_work_type = :no_dependency 36 37 SORTING_PREFERENCE_FIELD = :merge_requests_sort 38 39 ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = { 40 'Ci::CompareMetricsReportsService' => ->(project) { true }, 41 'Ci::CompareCodequalityReportsService' => ->(project) { true } 42 }.freeze 43 44 belongs_to :target_project, class_name: "Project" 45 belongs_to :source_project, class_name: "Project" 46 belongs_to :merge_user, class_name: "User" 47 belongs_to :iteration, foreign_key: 'sprint_id' 48 49 has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, 50 init: ->(mr, scope) do 51 if mr 52 mr.target_project&.merge_requests&.maximum(:iid) 53 elsif scope[:project] 54 where(target_project: scope[:project]).maximum(:iid) 55 end 56 end 57 58 has_many :merge_request_diffs, 59 -> { regular }, inverse_of: :merge_request 60 has_many :merge_request_context_commits, inverse_of: :merge_request 61 has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files 62 63 has_one :merge_request_diff, 64 -> { regular.order('merge_request_diffs.id DESC') }, inverse_of: :merge_request 65 has_one :merge_head_diff, 66 -> { merge_head }, inverse_of: :merge_request, class_name: 'MergeRequestDiff' 67 has_one :cleanup_schedule, inverse_of: :merge_request 68 69 belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff' 70 manual_inverse_association :latest_merge_request_diff, :merge_request 71 72 # This is the same as latest_merge_request_diff unless: 73 # 1. There are arguments - in which case we might be trying to force-reload. 74 # 2. This association is already loaded. 75 # 3. The latest diff does not exist. 76 # 4. It doesn't have any merge_request_diffs - it returns an empty MergeRequestDiff 77 # 78 # The second one in particular is important - MergeRequestDiff#merge_request 79 # is the inverse of MergeRequest#merge_request_diff, which means it may not be 80 # the latest diff, because we could have loaded any diff from this particular 81 # MR. If we haven't already loaded a diff, then it's fine to load the latest. 82 def merge_request_diff 83 fallback = latest_merge_request_diff unless association(:merge_request_diff).loaded? 84 85 fallback || super || MergeRequestDiff.new(merge_request_id: id) 86 end 87 88 belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline" 89 90 has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent 91 92 has_many :merge_requests_closing_issues, 93 class_name: 'MergeRequestsClosingIssues', 94 dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent 95 96 has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue 97 has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline' 98 has_many :suggestions, through: :notes 99 has_many :unresolved_notes, -> { unresolved }, as: :noteable, class_name: 'Note' 100 101 has_many :merge_request_assignees 102 has_many :assignees, class_name: "User", through: :merge_request_assignees 103 has_many :merge_request_reviewers 104 has_many :reviewers, class_name: "User", through: :merge_request_reviewers 105 has_many :user_mentions, class_name: "MergeRequestUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent 106 107 has_many :deployment_merge_requests 108 109 # These are deployments created after the merge request has been merged, and 110 # the merge request was tracked explicitly (instead of implicitly using a CI 111 # build). 112 has_many :deployments, 113 through: :deployment_merge_requests 114 115 has_many :draft_notes 116 has_many :reviews, inverse_of: :merge_request 117 118 KNOWN_MERGE_PARAMS = [ 119 :auto_merge_strategy, 120 :should_remove_source_branch, 121 :force_remove_source_branch, 122 :commit_message, 123 :squash_commit_message, 124 :sha 125 ].freeze 126 serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize 127 128 before_validation :set_draft_status 129 130 after_create :ensure_merge_request_diff 131 after_update :clear_memoized_shas 132 after_update :reload_diff_if_branch_changed 133 after_commit :ensure_metrics, on: [:create, :update], unless: :importing? 134 after_commit :expire_etag_cache, unless: :importing? 135 136 # When this attribute is true some MR validation is ignored 137 # It allows us to close or modify broken merge requests 138 attr_accessor :allow_broken 139 140 # Temporary fields to store compare vars 141 # when creating new merge request 142 attr_accessor :can_be_created, :compare_commits, :diff_options, :compare 143 144 participant :reviewers 145 146 # Keep states definition to be evaluated before the state_machine block to avoid spec failures. 147 # If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil. 148 def self.available_state_names 149 super + [:merged, :locked] 150 end 151 152 state_machine :state_id, initial: :opened, initialize: false do 153 event :close do 154 transition [:opened] => :closed 155 end 156 157 event :mark_as_merged do 158 transition [:opened, :locked] => :merged 159 end 160 161 event :reopen do 162 transition closed: :opened 163 end 164 165 event :lock_mr do 166 transition [:opened] => :locked 167 end 168 169 event :unlock_mr do 170 transition locked: :opened 171 end 172 173 before_transition any => :opened do |merge_request| 174 merge_request.merge_jid = nil 175 end 176 177 after_transition any => :opened do |merge_request| 178 merge_request.run_after_commit do 179 UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) 180 end 181 end 182 183 state :opened, value: MergeRequest.available_states[:opened] 184 state :closed, value: MergeRequest.available_states[:closed] 185 state :merged, value: MergeRequest.available_states[:merged] 186 state :locked, value: MergeRequest.available_states[:locked] 187 end 188 189 # Alias to state machine .with_state_id method 190 # This needs to be defined after the state machine block to avoid errors 191 class << self 192 alias_method :with_state, :with_state_id 193 alias_method :with_states, :with_state_ids 194 end 195 196 state_machine :merge_status, initial: :unchecked do 197 event :mark_as_preparing do 198 transition unchecked: :preparing 199 end 200 201 event :mark_as_unchecked do 202 transition [:preparing, :can_be_merged, :checking] => :unchecked 203 transition [:cannot_be_merged, :cannot_be_merged_rechecking] => :cannot_be_merged_recheck 204 end 205 206 event :mark_as_checking do 207 transition unchecked: :checking 208 transition cannot_be_merged_recheck: :cannot_be_merged_rechecking 209 end 210 211 event :mark_as_mergeable do 212 transition [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking] => :can_be_merged 213 end 214 215 event :mark_as_unmergeable do 216 transition [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking] => :cannot_be_merged 217 end 218 219 state :preparing 220 state :unchecked 221 state :cannot_be_merged_recheck 222 state :checking 223 state :cannot_be_merged_rechecking 224 state :can_be_merged 225 state :cannot_be_merged 226 227 around_transition do |merge_request, transition, block| 228 Gitlab::Timeless.timeless(merge_request, &block) 229 end 230 231 # rubocop: disable CodeReuse/ServiceClass 232 after_transition [:unchecked, :checking] => :cannot_be_merged do |merge_request, transition| 233 if merge_request.notify_conflict? 234 NotificationService.new.merge_request_unmergeable(merge_request) 235 TodoService.new.merge_request_became_unmergeable(merge_request) 236 end 237 end 238 # rubocop: enable CodeReuse/ServiceClass 239 240 def check_state?(merge_status) 241 [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking].include?(merge_status.to_sym) 242 end 243 end 244 245 # Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking` 246 # to avoid exposing unnecessary internal state 247 def public_merge_status 248 cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status 249 end 250 251 validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?] 252 validates :source_branch, presence: true 253 validates :target_project, presence: true 254 validates :target_branch, presence: true 255 validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing? 256 validate :validate_branches, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?] 257 validate :validate_fork, unless: :closed_or_merged_without_fork? 258 validate :validate_target_project, on: :create 259 260 scope :by_source_or_target_branch, ->(branch_name) do 261 where("source_branch = :branch OR target_branch = :branch", branch: branch_name) 262 end 263 scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } 264 scope :of_projects, ->(ids) { where(target_project_id: ids) } 265 scope :from_project, ->(project) { where(source_project_id: project.id) } 266 scope :from_fork, -> { where('source_project_id <> target_project_id') } 267 scope :from_and_to_forks, ->(project) do 268 from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id) 269 end 270 scope :merged, -> { with_state(:merged) } 271 scope :open_and_closed, -> { with_states(:opened, :closed) } 272 scope :drafts, -> { where(draft: true) } 273 scope :from_source_branches, ->(branches) { where(source_branch: branches) } 274 scope :by_commit_sha, ->(sha) do 275 where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil) 276 end 277 scope :by_merge_commit_sha, -> (sha) do 278 where(merge_commit_sha: sha) 279 end 280 scope :by_squash_commit_sha, -> (sha) do 281 where(squash_commit_sha: sha) 282 end 283 scope :by_merge_or_squash_commit_sha, -> (sha) do 284 from_union([by_squash_commit_sha(sha), by_merge_commit_sha(sha)]) 285 end 286 scope :by_related_commit_sha, -> (sha) do 287 from_union( 288 [ 289 by_commit_sha(sha), 290 by_squash_commit_sha(sha), 291 by_merge_commit_sha(sha) 292 ] 293 ) 294 end 295 scope :join_project, -> { joins(:target_project) } 296 scope :join_metrics, -> (target_project_id = nil) do 297 # Do not join the relation twice 298 return self if self.arel.join_sources.any? { |join| join.left.try(:name).eql?(MergeRequest::Metrics.table_name) } 299 300 query = joins(:metrics) 301 302 if !target_project_id && self.where_values_hash["target_project_id"] 303 target_project_id = self.where_values_hash["target_project_id"] 304 query = query.unscope(where: :target_project_id) 305 end 306 307 project_condition = if target_project_id 308 MergeRequest::Metrics.arel_table[:target_project_id].eq(target_project_id) 309 else 310 MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]) 311 end 312 313 query.where(project_condition) 314 end 315 scope :references_project, -> { references(:target_project) } 316 scope :with_api_entity_associations, -> { 317 preload_routables 318 .preload(:assignees, :author, :unresolved_notes, :labels, :milestone, 319 :timelogs, :latest_merge_request_diff, :reviewers, 320 target_project: :project_feature, 321 metrics: [:latest_closed_by, :merged_by]) 322 } 323 324 scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) } 325 scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) } 326 327 scope :by_target_branch_wildcard, ->(wildcard_branch_name) do 328 where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%')) 329 end 330 scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) } 331 scope :order_by_metric, ->(metric, direction) do 332 reverse_direction = { 'ASC' => 'DESC', 'DESC' => 'ASC' } 333 reversed_direction = reverse_direction[direction] || raise("Unknown sort direction was given: #{direction}") 334 335 order = Gitlab::Pagination::Keyset::Order.build([ 336 Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( 337 attribute_name: "merge_request_metrics_#{metric}", 338 column_expression: MergeRequest::Metrics.arel_table[metric], 339 order_expression: Gitlab::Database.nulls_last_order("merge_request_metrics.#{metric}", direction), 340 reversed_order_expression: Gitlab::Database.nulls_first_order("merge_request_metrics.#{metric}", reversed_direction), 341 order_direction: direction, 342 nullable: :nulls_last, 343 distinct: false, 344 add_to_projections: true 345 ), 346 Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( 347 attribute_name: 'merge_request_metrics_id', 348 order_expression: MergeRequest::Metrics.arel_table[:id].desc, 349 add_to_projections: true 350 ) 351 ]) 352 353 order.apply_cursor_conditions(join_metrics).order(order) 354 end 355 scope :order_merged_at_asc, -> { order_by_metric(:merged_at, 'ASC') } 356 scope :order_merged_at_desc, -> { order_by_metric(:merged_at, 'DESC') } 357 scope :order_closed_at_asc, -> { order_by_metric(:latest_closed_at, 'ASC') } 358 scope :order_closed_at_desc, -> { order_by_metric(:latest_closed_at, 'DESC') } 359 scope :preload_source_project, -> { preload(:source_project) } 360 scope :preload_target_project, -> { preload(:target_project) } 361 scope :preload_routables, -> do 362 preload(target_project: [:route, { namespace: :route }], 363 source_project: [:route, { namespace: :route }]) 364 end 365 scope :preload_author, -> { preload(:author) } 366 scope :preload_approved_by_users, -> { preload(:approved_by_users) } 367 scope :preload_metrics, -> (relation) { preload(metrics: relation) } 368 scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) } 369 scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: { merge_request_diff_commits: [:commit_author, :committer] }) } 370 scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) } 371 372 scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) } 373 374 scope :with_auto_merge_enabled, -> do 375 with_state(:opened).where(auto_merge_enabled: true) 376 end 377 378 scope :including_metrics, -> do 379 includes(:metrics) 380 end 381 382 scope :with_jira_issue_keys, -> { where('title ~ :regex OR merge_requests.description ~ :regex', regex: Gitlab::Regex.jira_issue_key_regex.source) } 383 384 scope :review_requested, -> do 385 where(reviewers_subquery.exists) 386 end 387 388 scope :no_review_requested, -> do 389 where(reviewers_subquery.exists.not) 390 end 391 392 scope :review_requested_to, ->(user) do 393 where( 394 reviewers_subquery 395 .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id)) 396 .exists 397 ) 398 end 399 400 scope :no_review_requested_to, ->(user) do 401 where( 402 reviewers_subquery 403 .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id)) 404 .exists 405 .not 406 ) 407 end 408 409 def self.total_time_to_merge 410 join_metrics 411 .merge(MergeRequest::Metrics.with_valid_time_to_merge) 412 .pluck(MergeRequest::Metrics.time_to_merge_expression) 413 .first 414 end 415 416 after_save :keep_around_commit, unless: :importing? 417 418 alias_attribute :project, :target_project 419 alias_attribute :project_id, :target_project_id 420 421 # Currently, `merge_when_pipeline_succeeds` column is used as a flag 422 # to check if _any_ auto merge strategy is activated on the merge request. 423 # Today, we have multiple strategies and MWPS is one of them. 424 # we'd eventually rename the column for avoiding confusions, but in the mean time 425 # please use `auto_merge_enabled` alias instead of `merge_when_pipeline_succeeds`. 426 alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds 427 alias_method :issuing_parent, :target_project 428 429 delegate :builds_with_coverage, to: :head_pipeline, prefix: true, allow_nil: true 430 431 RebaseLockTimeout = Class.new(StandardError) 432 433 def self.reference_prefix 434 '!' 435 end 436 437 # Returns the top 100 target branches 438 # 439 # The returned value is a Array containing branch names 440 # sort by updated_at of merge request: 441 # 442 # ['master', 'develop', 'production'] 443 # 444 # limit - The maximum number of target branch to return. 445 def self.recent_target_branches(limit: 100) 446 group(:target_branch) 447 .select(:target_branch) 448 .reorder(arel_table[:updated_at].maximum.desc) 449 .limit(limit) 450 .pluck(:target_branch) 451 end 452 453 def self.sort_by_attribute(method, excluded_labels: []) 454 case method.to_s 455 when 'merged_at', 'merged_at_asc' then order_merged_at_asc 456 when 'closed_at', 'closed_at_asc' then order_closed_at_asc 457 when 'merged_at_desc' then order_merged_at_desc 458 when 'closed_at_desc' then order_closed_at_desc 459 else 460 super 461 end 462 end 463 464 def self.reviewers_subquery 465 MergeRequestReviewer.arel_table 466 .project('true') 467 .where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id")) 468 end 469 470 def rebase_in_progress? 471 rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid) 472 end 473 474 # Use this method whenever you need to make sure the head_pipeline is synced with the 475 # branch head commit, for example checking if a merge request can be merged. 476 # For more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/40004 477 def actual_head_pipeline 478 head_pipeline&.matches_sha_or_source_sha?(diff_head_sha) ? head_pipeline : nil 479 end 480 481 def merge_pipeline 482 return unless merged? 483 484 # When the merge_method is :merge there will be a merge_commit_sha, however 485 # when it is fast-forward there is no merge commit, so we must fall back to 486 # either the squash commit (if the MR was squashed) or the diff head commit. 487 sha = merge_commit_sha || squash_commit_sha || diff_head_sha 488 target_project.latest_pipeline(target_branch, sha) 489 end 490 491 def head_pipeline_active? 492 !!head_pipeline&.active? 493 end 494 495 def actual_head_pipeline_active? 496 !!actual_head_pipeline&.active? 497 end 498 499 def actual_head_pipeline_success? 500 !!actual_head_pipeline&.success? 501 end 502 503 # Pattern used to extract `!123` merge request references from text 504 # 505 # This pattern supports cross-project references. 506 def self.reference_pattern 507 @reference_pattern ||= %r{ 508 (#{Project.reference_pattern})? 509 #{Regexp.escape(reference_prefix)}#{Gitlab::Regex.merge_request} 510 }x 511 end 512 513 def self.link_reference_pattern 514 @link_reference_pattern ||= super("merge_requests", Gitlab::Regex.merge_request) 515 end 516 517 def self.reference_valid?(reference) 518 reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE 519 end 520 521 def self.project_foreign_key 522 'target_project_id' 523 end 524 525 # Returns all the merge requests from an ActiveRecord:Relation. 526 # 527 # This method uses a UNION as it usually operates on the result of 528 # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries 529 # using multiple sub-queries especially when combined with an OR statement. 530 # UNIONs on the other hand perform much better in these cases. 531 # 532 # relation - An ActiveRecord::Relation that returns a list of Projects. 533 # 534 # Returns an ActiveRecord::Relation. 535 def self.in_projects(relation) 536 # unscoping unnecessary conditions that'll be applied 537 # when executing `where("merge_requests.id IN (#{union.to_sql})")` 538 source = unscoped.where(source_project_id: relation) 539 target = unscoped.where(target_project_id: relation) 540 541 from_union([source, target]) 542 end 543 544 # This is used after project import, to reset the IDs to the correct 545 # values. It is not intended to be called without having already scoped the 546 # relation. 547 # 548 # Only set `regular` merge request diffs as latest so `merge_head` diff 549 # won't be considered as `MergeRequest#merge_request_diff`. 550 def self.set_latest_merge_request_diff_ids! 551 update = " 552 latest_merge_request_diff_id = ( 553 SELECT MAX(id) 554 FROM merge_request_diffs 555 WHERE merge_requests.id = merge_request_diffs.merge_request_id 556 AND merge_request_diffs.diff_type = #{MergeRequestDiff.diff_types[:regular]} 557 )".squish 558 559 self.each_batch do |batch| 560 batch.update_all(update) 561 end 562 end 563 564 # WIP is deprecated in favor of Draft. Currently both options are supported 565 # https://gitlab.com/gitlab-org/gitlab/-/issues/227426 566 DRAFT_REGEX = /\A*#{Regexp.union(Gitlab::Regex.merge_request_wip, Gitlab::Regex.merge_request_draft)}+\s*/i.freeze 567 568 def self.work_in_progress?(title) 569 !!(title =~ DRAFT_REGEX) 570 end 571 572 def self.wipless_title(title) 573 title.sub(DRAFT_REGEX, "") 574 end 575 576 def self.wip_title(title) 577 work_in_progress?(title) ? title : "Draft: #{title}" 578 end 579 580 def self.participant_includes 581 [:reviewers, :award_emoji] + super 582 end 583 584 def committers 585 @committers ||= commits.committers 586 end 587 588 # Verifies if title has changed not taking into account Draft prefix 589 # for merge requests. 590 def wipless_title_changed(old_title) 591 self.class.wipless_title(old_title) != self.wipless_title 592 end 593 594 def hook_attrs 595 Gitlab::HookData::MergeRequestBuilder.new(self).build 596 end 597 598 # `from` argument can be a Namespace or Project. 599 def to_reference(from = nil, full: false) 600 reference = "#{self.class.reference_prefix}#{iid}" 601 602 "#{project.to_reference_base(from, full: full)}#{reference}" 603 end 604 605 def context_commits(limit: nil) 606 @context_commits ||= merge_request_context_commits.order_by_committed_date_desc.limit(limit).map(&:to_commit) 607 end 608 609 def recent_context_commits 610 context_commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE) 611 end 612 613 def context_commits_count 614 context_commits.count 615 end 616 617 def commits(limit: nil, load_from_gitaly: false) 618 return merge_request_diff.commits(limit: limit, load_from_gitaly: load_from_gitaly) if merge_request_diff.persisted? 619 620 commits_arr = if compare_commits 621 reversed_commits = compare_commits.reverse 622 limit ? reversed_commits.take(limit) : reversed_commits 623 else 624 [] 625 end 626 627 CommitCollection.new(source_project, commits_arr, source_branch) 628 end 629 630 def recent_commits(load_from_gitaly: false) 631 commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE, load_from_gitaly: load_from_gitaly) 632 end 633 634 def commits_count 635 if merge_request_diff.persisted? 636 merge_request_diff.commits_count 637 elsif compare_commits 638 compare_commits.size 639 else 640 0 641 end 642 end 643 644 def commit_shas(limit: nil) 645 return merge_request_diff.commit_shas(limit: limit) if merge_request_diff.persisted? 646 647 shas = 648 if compare_commits 649 compare_commits.to_a.reverse.map(&:sha) 650 else 651 Array(diff_head_sha) 652 end 653 654 limit ? shas.take(limit) : shas 655 end 656 657 def supports_suggestion? 658 true 659 end 660 661 # Calls `MergeWorker` to proceed with the merge process and 662 # updates `merge_jid` with the MergeWorker#jid. 663 # This helps tracking enqueued and ongoing merge jobs. 664 def merge_async(user_id, params) 665 jid = MergeWorker.with_status.perform_async(id, user_id, params.to_h) 666 update_column(:merge_jid, jid) 667 668 # merge_ongoing? depends on merge_jid 669 # expire etag cache since the attribute is changed without triggering callbacks 670 expire_etag_cache 671 end 672 673 # Set off a rebase asynchronously, atomically updating the `rebase_jid` of 674 # the MR so that the status of the operation can be tracked. 675 def rebase_async(user_id, skip_ci: false) 676 with_rebase_lock do 677 raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress? 678 679 # Although there is a race between setting rebase_jid here and clearing it 680 # in the RebaseWorker, it can't do any harm since we check both that the 681 # attribute is set *and* that the sidekiq job is still running. So a JID 682 # for a completed RebaseWorker is equivalent to a nil JID. 683 jid = Sidekiq::Worker.skipping_transaction_check do 684 RebaseWorker.with_status.perform_async(id, user_id, skip_ci) 685 end 686 687 update_column(:rebase_jid, jid) 688 end 689 690 # rebase_in_progress? depends on rebase_jid 691 # expire etag cache since the attribute is changed without triggering callbacks 692 expire_etag_cache 693 end 694 695 def merge_participants 696 participants = [author] 697 698 if auto_merge_enabled? && !participants.include?(merge_user) 699 participants << merge_user 700 end 701 702 participants.select { |participant| Ability.allowed?(participant, :read_merge_request, self) } 703 end 704 705 def first_commit 706 compare_commits.present? ? compare_commits.first : merge_request_diff.first_commit 707 end 708 709 def raw_diffs(*args) 710 compare.present? ? compare.raw_diffs(*args) : merge_request_diff.raw_diffs(*args) 711 end 712 713 def diffs(diff_options = {}) 714 if compare 715 # When saving MR diffs, `expanded` is implicitly added (because we need 716 # to save the entire contents to the DB), so add that here for 717 # consistency. 718 compare.diffs(diff_options.merge(expanded: true)) 719 else 720 merge_request_diff.diffs(diff_options) 721 end 722 end 723 724 def non_latest_diffs 725 merge_request_diffs.where.not(id: merge_request_diff.id) 726 end 727 728 def note_positions_for_paths(paths, user = nil) 729 positions = notes.new_diff_notes.joins(:note_diff_file) 730 .where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths) 731 .positions 732 733 collection = Gitlab::Diff::PositionCollection.new(positions, diff_head_sha) 734 735 return collection unless user 736 737 positions = draft_notes 738 .authored_by(user) 739 .positions 740 .select { |pos| paths.include?(pos.file_path) } 741 742 collection.concat(positions) 743 end 744 745 def preloads_discussion_diff_highlighting? 746 true 747 end 748 749 def discussions_diffs 750 strong_memoize(:discussions_diffs) do 751 note_diff_files = NoteDiffFile 752 .joins(:diff_note) 753 .merge(notes.or(commit_notes)) 754 .includes(diff_note: :project) 755 756 Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a) 757 end 758 end 759 760 def diff_stats 761 return unless diff_refs 762 763 strong_memoize(:diff_stats) do 764 project.repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha) 765 end 766 end 767 768 def diff_size 769 # Calling `merge_request_diff.diffs.real_size` will also perform 770 # highlighting, which we don't need here. 771 merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size 772 end 773 774 def modified_paths(past_merge_request_diff: nil, fallback_on_overflow: false) 775 if past_merge_request_diff 776 past_merge_request_diff.modified_paths(fallback_on_overflow: fallback_on_overflow) 777 elsif compare 778 diff_stats&.paths || compare.modified_paths 779 else 780 merge_request_diff.modified_paths(fallback_on_overflow: fallback_on_overflow) 781 end 782 end 783 784 def new_paths 785 diffs.diff_files.map(&:new_path) 786 end 787 788 def diff_base_commit 789 if merge_request_diff.persisted? 790 merge_request_diff.base_commit 791 else 792 branch_merge_base_commit 793 end 794 end 795 796 def diff_start_commit 797 if merge_request_diff.persisted? 798 merge_request_diff.start_commit 799 else 800 target_branch_head 801 end 802 end 803 804 def diff_head_commit 805 if merge_request_diff.persisted? 806 merge_request_diff.head_commit 807 else 808 source_branch_head 809 end 810 end 811 812 def diff_start_sha 813 if merge_request_diff.persisted? 814 merge_request_diff.start_commit_sha 815 else 816 target_branch_head.try(:sha) 817 end 818 end 819 820 def diff_base_sha 821 if merge_request_diff.persisted? 822 merge_request_diff.base_commit_sha 823 else 824 branch_merge_base_commit.try(:sha) 825 end 826 end 827 828 def diff_head_sha 829 if merge_request_diff.persisted? 830 merge_request_diff.head_commit_sha 831 else 832 source_branch_head.try(:sha) 833 end 834 end 835 836 # When importing a pull request from GitHub, the old and new branches may no 837 # longer actually exist by those names, but we need to recreate the merge 838 # request diff with the right source and target shas. 839 # We use these attributes to force these to the intended values. 840 attr_writer :target_branch_sha, :source_branch_sha 841 842 def source_branch_ref 843 return @source_branch_sha if @source_branch_sha 844 return unless source_branch 845 846 Gitlab::Git::BRANCH_REF_PREFIX + source_branch 847 end 848 849 def target_branch_ref 850 return @target_branch_sha if @target_branch_sha 851 return unless target_branch 852 853 Gitlab::Git::BRANCH_REF_PREFIX + target_branch 854 end 855 856 def source_branch_head 857 strong_memoize(:source_branch_head) do 858 if source_project && source_branch_ref 859 source_project.repository.commit(source_branch_ref) 860 end 861 end 862 end 863 864 def target_branch_head 865 strong_memoize(:target_branch_head) do 866 target_project.repository.commit(target_branch_ref) 867 end 868 end 869 870 def branch_merge_base_commit 871 start_sha = target_branch_sha 872 head_sha = source_branch_sha 873 874 if start_sha && head_sha 875 target_project.merge_base_commit(start_sha, head_sha) 876 end 877 end 878 879 def target_branch_sha 880 @target_branch_sha || target_branch_head.try(:sha) 881 end 882 883 def source_branch_sha 884 @source_branch_sha || source_branch_head.try(:sha) 885 end 886 887 def diff_refs 888 if importing? || persisted? 889 merge_request_diff.diff_refs 890 else 891 repository_diff_refs 892 end 893 end 894 895 # Instead trying to fetch the 896 # persisted diff_refs, this method goes 897 # straight to the repository to get the 898 # most recent data possible. 899 def repository_diff_refs 900 Gitlab::Diff::DiffRefs.new( 901 base_sha: branch_merge_base_sha, 902 start_sha: target_branch_sha, 903 head_sha: source_branch_sha 904 ) 905 end 906 907 def branch_merge_base_sha 908 branch_merge_base_commit.try(:sha) 909 end 910 911 def validate_branches 912 return unless target_project && source_project 913 914 if target_project == source_project && target_branch == source_branch 915 errors.add :branch_conflict, "You can't use same project/branch for source and target" 916 return 917 end 918 919 [:source_branch, :target_branch].each { |attr| validate_branch_name(attr) } 920 921 if opened? 922 similar_mrs = target_project 923 .merge_requests 924 .where(source_branch: source_branch, target_branch: target_branch) 925 .where(source_project_id: source_project&.id) 926 .opened 927 928 similar_mrs = similar_mrs.where.not(id: id) if persisted? 929 930 conflict = similar_mrs.first 931 932 if conflict.present? 933 errors.add( 934 :validate_branches, 935 "Another open merge request already exists for this source branch: #{conflict.to_reference}" 936 ) 937 end 938 end 939 end 940 941 def validate_branch_name(attr) 942 return unless will_save_change_to_attribute?(attr) 943 944 branch = read_attribute(attr) 945 946 return unless branch 947 948 errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch) 949 end 950 951 def validate_target_project 952 return true if target_project.merge_requests_enabled? 953 954 errors.add :base, 'Target project has disabled merge requests' 955 end 956 957 def validate_fork 958 return true unless target_project && source_project 959 return true if target_project == source_project 960 return true unless source_project_missing? 961 962 errors.add :validate_fork, 963 'Source project is not a fork of the target project' 964 end 965 966 def merge_ongoing? 967 # While the MergeRequest is locked, it should present itself as 'merge ongoing'. 968 # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron. 969 return true if locked? 970 971 !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) 972 end 973 974 def closed_or_merged_without_fork? 975 (closed? || merged?) && source_project_missing? 976 end 977 978 def source_project_missing? 979 return false unless for_fork? 980 return true unless source_project 981 982 !source_project.in_fork_network_of?(target_project) 983 end 984 985 def reopenable? 986 closed? && !source_project_missing? && source_branch_exists? 987 end 988 989 def can_be_closed? 990 opened? 991 end 992 993 def ensure_merge_request_diff 994 merge_request_diff.persisted? || create_merge_request_diff 995 end 996 997 def create_merge_request_diff 998 fetch_ref! 999 1000 # n+1: https://gitlab.com/gitlab-org/gitlab/-/issues/19377 1001 Gitlab::GitalyClient.allow_n_plus_1_calls do 1002 merge_request_diffs.create! 1003 reload_merge_request_diff 1004 end 1005 end 1006 1007 def viewable_diffs 1008 @viewable_diffs ||= merge_request_diffs.viewable.to_a 1009 end 1010 1011 def merge_request_diff_for(diff_refs_or_sha) 1012 matcher = 1013 if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs) 1014 { 1015 'start_commit_sha' => diff_refs_or_sha.start_sha, 1016 'head_commit_sha' => diff_refs_or_sha.head_sha, 1017 'base_commit_sha' => diff_refs_or_sha.base_sha 1018 } 1019 else 1020 { 'head_commit_sha' => diff_refs_or_sha } 1021 end 1022 1023 viewable_diffs.find do |diff| 1024 diff.attributes.slice(*matcher.keys) == matcher 1025 end 1026 end 1027 1028 def version_params_for(diff_refs) 1029 if diff = merge_request_diff_for(diff_refs) 1030 { diff_id: diff.id } 1031 elsif diff = merge_request_diff_for(diff_refs.head_sha) 1032 { 1033 diff_id: diff.id, 1034 start_sha: diff_refs.start_sha 1035 } 1036 end 1037 end 1038 1039 def clear_memoized_shas 1040 @target_branch_sha = @source_branch_sha = nil 1041 1042 clear_memoization(:source_branch_head) 1043 clear_memoization(:target_branch_head) 1044 end 1045 1046 def reload_diff_if_branch_changed 1047 if (saved_change_to_source_branch? || saved_change_to_target_branch?) && 1048 (source_branch_head && target_branch_head) 1049 reload_diff 1050 end 1051 end 1052 1053 # rubocop: disable CodeReuse/ServiceClass 1054 def reload_diff(current_user = nil) 1055 return unless open? 1056 1057 MergeRequests::ReloadDiffsService.new(self, current_user).execute 1058 end 1059 1060 def check_mergeability(async: false) 1061 return unless recheck_merge_status? 1062 1063 check_service = MergeRequests::MergeabilityCheckService.new(self) 1064 1065 if async 1066 check_service.async_execute 1067 else 1068 check_service.execute(retry_lease: false) 1069 end 1070 end 1071 # rubocop: enable CodeReuse/ServiceClass 1072 1073 def diffable_merge_ref? 1074 open? && merge_head_diff.present? && (Feature.enabled?(:display_merge_conflicts_in_diff, project) || can_be_merged?) 1075 end 1076 1077 # Returns boolean indicating the merge_status should be rechecked in order to 1078 # switch to either can_be_merged or cannot_be_merged. 1079 def recheck_merge_status? 1080 self.class.state_machines[:merge_status].check_state?(merge_status) 1081 end 1082 1083 def merge_event 1084 @merge_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :merged).last 1085 end 1086 1087 def closed_event 1088 @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: :closed).last 1089 end 1090 1091 def work_in_progress? 1092 self.class.work_in_progress?(title) 1093 end 1094 alias_method :draft?, :work_in_progress? 1095 1096 def wipless_title 1097 self.class.wipless_title(self.title) 1098 end 1099 1100 def wip_title 1101 self.class.wip_title(self.title) 1102 end 1103 1104 def mergeable?(skip_ci_check: false, skip_discussions_check: false) 1105 return false unless mergeable_state?(skip_ci_check: skip_ci_check, 1106 skip_discussions_check: skip_discussions_check) 1107 1108 check_mergeability 1109 1110 can_be_merged? && !should_be_rebased? 1111 end 1112 1113 # rubocop: disable CodeReuse/ServiceClass 1114 def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) 1115 return false unless open? 1116 return false if work_in_progress? 1117 return false if broken? 1118 return false unless skip_discussions_check || mergeable_discussions_state? 1119 1120 if Feature.enabled?(:improved_mergeability_checks, self.project, default_enabled: :yaml) 1121 additional_checks = MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: { skip_ci_check: skip_ci_check }) 1122 additional_checks.execute.all?(&:success?) 1123 else 1124 return false unless skip_ci_check || mergeable_ci_state? 1125 1126 true 1127 end 1128 end 1129 # rubocop: enable CodeReuse/ServiceClass 1130 1131 def ff_merge_possible? 1132 project.repository.ancestor?(target_branch_sha, diff_head_sha) 1133 end 1134 1135 def should_be_rebased? 1136 project.ff_merge_must_be_possible? && !ff_merge_possible? 1137 end 1138 1139 def can_cancel_auto_merge?(current_user) 1140 can_be_merged_by?(current_user) || self.author == current_user 1141 end 1142 1143 def can_remove_source_branch?(current_user) 1144 source_project && 1145 !ProtectedBranch.protected?(source_project, source_branch) && 1146 !source_project.root_ref?(source_branch) && 1147 Ability.allowed?(current_user, :push_code, source_project) && 1148 diff_head_sha == source_branch_head.try(:sha) 1149 end 1150 1151 def should_remove_source_branch? 1152 Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch']) 1153 end 1154 1155 def force_remove_source_branch? 1156 Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch']) 1157 end 1158 1159 def auto_merge_strategy 1160 return unless auto_merge_enabled? 1161 1162 merge_params['auto_merge_strategy'] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS 1163 end 1164 1165 def auto_merge_strategy=(strategy) 1166 merge_params['auto_merge_strategy'] = strategy 1167 end 1168 1169 def remove_source_branch? 1170 should_remove_source_branch? || force_remove_source_branch? 1171 end 1172 1173 def notify_conflict? 1174 (opened? || locked?) && 1175 has_commits? && 1176 !branch_missing? && 1177 !project.repository.can_be_merged?(diff_head_sha, target_branch) 1178 rescue Gitlab::Git::CommandError 1179 # Checking mergeability can trigger exception, e.g. non-utf8 1180 # We ignore this type of errors. 1181 false 1182 end 1183 1184 def related_notes 1185 # We're using a UNION ALL here since this results in better performance 1186 # compared to using OR statements. We're using UNION ALL since the queries 1187 # used won't produce any duplicates (e.g. a note for a commit can't also be 1188 # a note for an MR). 1189 Note 1190 .from_union([notes, commit_notes], remove_duplicates: false) 1191 .includes(:noteable) 1192 end 1193 1194 alias_method :discussion_notes, :related_notes 1195 1196 def commit_notes 1197 # Fetch comments only from last 100 commits 1198 commit_ids = commit_shas(limit: 100) 1199 1200 Note 1201 .user 1202 .where(project_id: [source_project_id, target_project_id]) 1203 .for_commit_id(commit_ids) 1204 end 1205 1206 def mergeable_discussions_state? 1207 return true unless project.only_allow_merge_if_all_discussions_are_resolved? 1208 1209 unresolved_notes.none?(&:to_be_resolved?) 1210 end 1211 1212 def for_fork? 1213 target_project != source_project 1214 end 1215 1216 def for_same_project? 1217 target_project == source_project 1218 end 1219 1220 # If the merge request closes any issues, save this information in the 1221 # `MergeRequestsClosingIssues` model. This is a performance optimization. 1222 # Calculating this information for a number of merge requests requires 1223 # running `ReferenceExtractor` on each of them separately. 1224 # This optimization does not apply to issues from external sources. 1225 def cache_merge_request_closes_issues!(current_user = self.author) 1226 return unless project.issues_enabled? 1227 return if closed? || merged? 1228 1229 transaction do 1230 self.merge_requests_closing_issues.delete_all 1231 1232 closes_issues(current_user).each do |issue| 1233 next if issue.is_a?(ExternalIssue) 1234 1235 self.merge_requests_closing_issues.create!(issue: issue) 1236 end 1237 end 1238 end 1239 1240 def visible_closing_issues_for(current_user = self.author) 1241 strong_memoize(:visible_closing_issues_for) do 1242 if self.target_project.has_external_issue_tracker? 1243 closes_issues(current_user) 1244 else 1245 cached_closes_issues.select do |issue| 1246 Ability.allowed?(current_user, :read_issue, issue) 1247 end 1248 end 1249 end 1250 end 1251 1252 # Return the set of issues that will be closed if this merge request is accepted. 1253 def closes_issues(current_user = self.author) 1254 if target_branch == project.default_branch 1255 messages = [title, description] 1256 messages.concat(commits.map(&:safe_message)) if merge_request_diff.persisted? 1257 1258 Gitlab::ClosingIssueExtractor.new(project, current_user) 1259 .closed_by_message(messages.join("\n")) 1260 else 1261 [] 1262 end 1263 end 1264 1265 def issues_mentioned_but_not_closing(current_user) 1266 return [] unless target_branch == project.default_branch 1267 1268 ext = Gitlab::ReferenceExtractor.new(project, current_user) 1269 ext.analyze("#{title}\n#{description}") 1270 1271 ext.issues - visible_closing_issues_for(current_user) 1272 end 1273 1274 def target_project_path 1275 if target_project 1276 target_project.full_path 1277 else 1278 "(removed)" 1279 end 1280 end 1281 1282 def source_project_path 1283 if source_project 1284 source_project.full_path 1285 else 1286 "(removed)" 1287 end 1288 end 1289 1290 def source_project_namespace 1291 if source_project && source_project.namespace 1292 source_project.namespace.full_path 1293 else 1294 "(removed)" 1295 end 1296 end 1297 1298 def target_project_namespace 1299 if target_project && target_project.namespace 1300 target_project.namespace.full_path 1301 else 1302 "(removed)" 1303 end 1304 end 1305 1306 def source_branch_exists? 1307 return false unless self.source_project 1308 1309 self.source_project.repository.branch_exists?(self.source_branch) 1310 end 1311 1312 def target_branch_exists? 1313 return false unless self.target_project 1314 1315 self.target_project.repository.branch_exists?(self.target_branch) 1316 end 1317 1318 def default_merge_commit_message(include_description: false) 1319 if self.target_project.merge_commit_template.present? && !include_description 1320 return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self).merge_message 1321 end 1322 1323 closes_issues_references = visible_closing_issues_for.map do |issue| 1324 issue.to_reference(target_project) 1325 end 1326 1327 message = [ 1328 "Merge branch '#{source_branch}' into '#{target_branch}'", 1329 title 1330 ] 1331 1332 if !include_description && closes_issues_references.present? 1333 message << "Closes #{closes_issues_references.to_sentence}" 1334 end 1335 1336 message << "#{description}" if include_description && description.present? 1337 message << "See merge request #{to_reference(full: true)}" 1338 1339 message.join("\n\n") 1340 end 1341 1342 def default_squash_commit_message 1343 if self.target_project.squash_commit_template.present? 1344 return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self).squash_message 1345 end 1346 1347 title 1348 end 1349 1350 # Returns the oldest multi-line commit 1351 def first_multiline_commit 1352 strong_memoize(:first_multiline_commit) do 1353 recent_commits.without_merge_commits.reverse_each.find(&:description?) 1354 end 1355 end 1356 1357 def squash_on_merge? 1358 return true if target_project.squash_always? 1359 return false if target_project.squash_never? 1360 1361 squash? 1362 end 1363 1364 def has_ci? 1365 return false if has_no_commits? 1366 1367 ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do 1368 !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_integration) 1369 end 1370 end 1371 1372 def branch_missing? 1373 !source_branch_exists? || !target_branch_exists? 1374 end 1375 1376 def broken? 1377 has_no_commits? || branch_missing? || cannot_be_merged? 1378 end 1379 1380 def can_be_merged_by?(user, skip_collaboration_check: false) 1381 access = ::Gitlab::UserAccess.new(user, container: project, skip_collaboration_check: skip_collaboration_check) 1382 access.can_update_branch?(target_branch) 1383 end 1384 1385 def can_be_merged_via_command_line_by?(user) 1386 access = ::Gitlab::UserAccess.new(user, container: project) 1387 access.can_push_to_branch?(target_branch) 1388 end 1389 1390 def mergeable_ci_state? 1391 return true unless project.only_allow_merge_if_pipeline_succeeds? 1392 return false unless actual_head_pipeline 1393 return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped? 1394 1395 actual_head_pipeline.success? 1396 end 1397 1398 def environments_for(current_user, latest: false) 1399 return [] unless diff_head_commit 1400 1401 envs = Environments::EnvironmentsByDeploymentsFinder.new(target_project, current_user, 1402 ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute 1403 1404 if source_project 1405 envs.concat Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user, 1406 ref: source_branch, commit: diff_head_commit, find_latest: latest).execute 1407 end 1408 1409 envs.uniq 1410 end 1411 1412 ## 1413 # This method is for looking for active environments which created via pipelines for merge requests. 1414 # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`), 1415 # we cannot look up environments with source branch name. 1416 def environments 1417 return Environment.none unless actual_head_pipeline&.merge_request? 1418 1419 build_for_actual_head_pipeline = Ci::Build.latest.where(pipeline: actual_head_pipeline) 1420 1421 environments = build_for_actual_head_pipeline.joins(:metadata) 1422 .where.not('ci_builds_metadata.expanded_environment_name' => nil) 1423 .distinct('ci_builds_metadata.expanded_environment_name') 1424 .limit(100) 1425 .pluck(:expanded_environment_name) 1426 1427 Environment.where(project: project, name: environments) 1428 end 1429 1430 def fetch_ref! 1431 target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path) 1432 end 1433 1434 # Returns the current merge-ref HEAD commit. 1435 # 1436 def merge_ref_head 1437 return project.repository.commit(merge_ref_sha) if merge_ref_sha 1438 1439 project.repository.commit(merge_ref_path) 1440 end 1441 1442 def ref_path 1443 "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" 1444 end 1445 1446 def merge_ref_path 1447 "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge" 1448 end 1449 1450 def train_ref_path 1451 "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/train" 1452 end 1453 1454 def cleanup_refs(only: :all) 1455 target_refs = [] 1456 target_refs << ref_path if %i[all head].include?(only) 1457 target_refs << merge_ref_path if %i[all merge].include?(only) 1458 target_refs << train_ref_path if %i[all train].include?(only) 1459 1460 project.repository.delete_refs(*target_refs) 1461 end 1462 1463 def self.merge_request_ref?(ref) 1464 ref.start_with?("refs/#{Repository::REF_MERGE_REQUEST}/") 1465 end 1466 1467 def self.merge_train_ref?(ref) 1468 %r{\Arefs/#{Repository::REF_MERGE_REQUEST}/\d+/train\z}.match?(ref) 1469 end 1470 1471 def in_locked_state 1472 lock_mr 1473 yield 1474 ensure 1475 unlock_mr 1476 end 1477 1478 def update_and_mark_in_progress_merge_commit_sha(commit_id) 1479 self.update(in_progress_merge_commit_sha: commit_id) 1480 # Since another process checks for matching merge request, we need 1481 # to make it possible to detect whether the query should go to the 1482 # primary. 1483 target_project.mark_primary_write_location 1484 end 1485 1486 def diverged_commits_count 1487 cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") 1488 1489 if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha 1490 cache = { 1491 source_sha: source_branch_sha, 1492 target_sha: target_branch_sha, 1493 diverged_commits_count: compute_diverged_commits_count 1494 } 1495 Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache) 1496 end 1497 1498 cache[:diverged_commits_count] 1499 end 1500 1501 def compute_diverged_commits_count 1502 return 0 unless source_branch_sha && target_branch_sha 1503 1504 target_project.repository 1505 .count_commits_between(source_branch_sha, target_branch_sha) 1506 end 1507 private :compute_diverged_commits_count 1508 1509 def diverged_from_target_branch? 1510 diverged_commits_count > 0 1511 end 1512 1513 def all_pipelines 1514 strong_memoize(:all_pipelines) do 1515 Ci::PipelinesForMergeRequestFinder.new(self, nil).all 1516 end 1517 end 1518 1519 def update_head_pipeline 1520 find_actual_head_pipeline.try do |pipeline| 1521 self.head_pipeline = pipeline 1522 update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed? 1523 end 1524 end 1525 1526 def has_test_reports? 1527 actual_head_pipeline&.has_reports?(Ci::JobArtifact.test_reports) 1528 end 1529 1530 def predefined_variables 1531 Gitlab::Ci::Variables::Collection.new.tap do |variables| 1532 variables.append(key: 'CI_MERGE_REQUEST_ID', value: id.to_s) 1533 variables.append(key: 'CI_MERGE_REQUEST_IID', value: iid.to_s) 1534 variables.append(key: 'CI_MERGE_REQUEST_REF_PATH', value: ref_path.to_s) 1535 variables.append(key: 'CI_MERGE_REQUEST_PROJECT_ID', value: project.id.to_s) 1536 variables.append(key: 'CI_MERGE_REQUEST_PROJECT_PATH', value: project.full_path) 1537 variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url) 1538 variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s) 1539 variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title) 1540 variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.present? 1541 variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone 1542 variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present? 1543 variables.concat(source_project_variables) 1544 end 1545 end 1546 1547 def compare_test_reports 1548 unless has_test_reports? 1549 return { status: :error, status_reason: 'This merge request does not have test reports' } 1550 end 1551 1552 compare_reports(Ci::CompareTestReportsService) 1553 end 1554 1555 def has_accessibility_reports? 1556 actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports) 1557 end 1558 1559 def has_coverage_reports? 1560 actual_head_pipeline&.has_coverage_reports? 1561 end 1562 1563 def has_terraform_reports? 1564 actual_head_pipeline&.has_reports?(Ci::JobArtifact.terraform_reports) 1565 end 1566 1567 def compare_accessibility_reports 1568 unless has_accessibility_reports? 1569 return { status: :error, status_reason: _('This merge request does not have accessibility reports') } 1570 end 1571 1572 compare_reports(Ci::CompareAccessibilityReportsService) 1573 end 1574 1575 # TODO: this method and compare_test_reports use the same 1576 # result type, which is handled by the controller's #reports_response. 1577 # we should minimize mistakes by isolating the common parts. 1578 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 1579 def find_coverage_reports 1580 unless has_coverage_reports? 1581 return { status: :error, status_reason: 'This merge request does not have coverage reports' } 1582 end 1583 1584 compare_reports(Ci::GenerateCoverageReportsService) 1585 end 1586 1587 def has_codequality_mr_diff_report? 1588 actual_head_pipeline&.has_codequality_mr_diff_report? 1589 end 1590 1591 # TODO: this method and compare_test_reports use the same 1592 # result type, which is handled by the controller's #reports_response. 1593 # we should minimize mistakes by isolating the common parts. 1594 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 1595 def find_codequality_mr_diff_reports 1596 unless has_codequality_mr_diff_report? 1597 return { status: :error, status_reason: 'This merge request does not have codequality mr diff reports' } 1598 end 1599 1600 compare_reports(Ci::GenerateCodequalityMrDiffReportService) 1601 end 1602 1603 def has_codequality_reports? 1604 actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports) 1605 end 1606 1607 def compare_codequality_reports 1608 unless has_codequality_reports? 1609 return { status: :error, status_reason: _('This merge request does not have codequality reports') } 1610 end 1611 1612 compare_reports(Ci::CompareCodequalityReportsService) 1613 end 1614 1615 def find_terraform_reports 1616 unless has_terraform_reports? 1617 return { status: :error, status_reason: 'This merge request does not have terraform reports' } 1618 end 1619 1620 compare_reports(Ci::GenerateTerraformReportsService) 1621 end 1622 1623 def has_exposed_artifacts? 1624 actual_head_pipeline&.has_exposed_artifacts? 1625 end 1626 1627 # TODO: this method and compare_test_reports use the same 1628 # result type, which is handled by the controller's #reports_response. 1629 # we should minimize mistakes by isolating the common parts. 1630 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 1631 def find_exposed_artifacts 1632 unless has_exposed_artifacts? 1633 return { status: :error, status_reason: 'This merge request does not have exposed artifacts' } 1634 end 1635 1636 compare_reports(Ci::GenerateExposedArtifactsReportService) 1637 end 1638 1639 # TODO: consider renaming this as with exposed artifacts we generate reports, 1640 # not always compare 1641 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 1642 def compare_reports(service_class, current_user = nil, report_type = nil ) 1643 with_reactive_cache(service_class.name, current_user&.id, report_type) do |data| 1644 unless service_class.new(project, current_user, id: id, report_type: report_type) 1645 .latest?(comparison_base_pipeline(service_class.name), actual_head_pipeline, data) 1646 raise InvalidateReactiveCache 1647 end 1648 1649 data 1650 end || { status: :parsing } 1651 end 1652 1653 def has_sast_reports? 1654 !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports) 1655 end 1656 1657 def has_secret_detection_reports? 1658 !!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports) 1659 end 1660 1661 def compare_sast_reports(current_user) 1662 return missing_report_error("SAST") unless has_sast_reports? 1663 1664 compare_reports(::Ci::CompareSecurityReportsService, current_user, 'sast') 1665 end 1666 1667 def compare_secret_detection_reports(current_user) 1668 return missing_report_error("secret detection") unless has_secret_detection_reports? 1669 1670 compare_reports(::Ci::CompareSecurityReportsService, current_user, 'secret_detection') 1671 end 1672 1673 def calculate_reactive_cache(identifier, current_user_id = nil, report_type = nil, *args) 1674 service_class = identifier.constantize 1675 1676 # TODO: the type check should change to something that includes exposed artifacts service 1677 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 1678 raise NameError, service_class unless service_class < Ci::CompareReportsBaseService 1679 1680 current_user = User.find_by(id: current_user_id) 1681 service_class.new(project, current_user, id: id, report_type: report_type).execute(comparison_base_pipeline(identifier), actual_head_pipeline) 1682 end 1683 1684 def recent_diff_head_shas(limit = 100) 1685 merge_request_diffs.recent(limit).pluck(:head_commit_sha) 1686 end 1687 1688 def all_commits 1689 MergeRequestDiffCommit 1690 .where(merge_request_diff: merge_request_diffs.recent) 1691 .limit(10_000) 1692 end 1693 1694 # Note that this could also return SHA from now dangling commits 1695 # 1696 def all_commit_shas 1697 @all_commit_shas ||= begin 1698 return commit_shas unless persisted? 1699 1700 all_commits.pluck(:sha).uniq 1701 end 1702 end 1703 1704 def merge_commit 1705 @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha 1706 end 1707 1708 def short_merge_commit_sha 1709 Commit.truncate_sha(merge_commit_sha) if merge_commit_sha 1710 end 1711 1712 def merged_commit_sha 1713 return unless merged? 1714 1715 sha = merge_commit_sha || squash_commit_sha || diff_head_sha 1716 sha.presence 1717 end 1718 1719 def short_merged_commit_sha 1720 if sha = merged_commit_sha 1721 Commit.truncate_sha(sha) 1722 end 1723 end 1724 1725 def can_be_reverted?(current_user) 1726 return false unless merge_commit 1727 return false unless merged_at 1728 1729 # It is not guaranteed that Note#created_at will be strictly later than 1730 # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this 1731 # comparison, as will a HA environment if clocks are not *precisely* 1732 # synchronized. Add a minute's leeway to compensate for both possibilities 1733 cutoff = merged_at - 1.minute 1734 1735 notes_association = notes_with_associations.where('created_at >= ?', cutoff) 1736 1737 !merge_commit.has_been_reverted?(current_user, notes_association) 1738 end 1739 1740 def merged_at 1741 strong_memoize(:merged_at) do 1742 next unless merged? 1743 1744 metrics&.merged_at || 1745 merge_event&.created_at || 1746 resource_state_events.find_by(state: :merged)&.created_at || 1747 notes.system.reorder(nil).find_by(note: 'merged')&.created_at 1748 end 1749 end 1750 1751 def can_be_cherry_picked? 1752 merge_commit.present? 1753 end 1754 1755 def has_complete_diff_refs? 1756 diff_refs && diff_refs.complete? 1757 end 1758 1759 # rubocop: disable CodeReuse/ServiceClass 1760 def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil) 1761 return unless has_complete_diff_refs? 1762 return if new_diff_refs == old_diff_refs 1763 1764 active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion| 1765 discussion.active?(old_diff_refs) 1766 end 1767 return if active_diff_discussions.empty? 1768 1769 paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq 1770 1771 service = Discussions::UpdateDiffPositionService.new( 1772 self.project, 1773 current_user, 1774 old_diff_refs: old_diff_refs, 1775 new_diff_refs: new_diff_refs, 1776 paths: paths 1777 ) 1778 1779 active_diff_discussions.each do |discussion| 1780 service.execute(discussion) 1781 end 1782 1783 if project.resolve_outdated_diff_discussions? 1784 MergeRequests::ResolvedDiscussionNotificationService 1785 .new(project: project, current_user: current_user) 1786 .execute(self) 1787 end 1788 end 1789 # rubocop: enable CodeReuse/ServiceClass 1790 1791 def keep_around_commit 1792 project.repository.keep_around(self.merge_commit_sha) 1793 end 1794 1795 def has_commits? 1796 merge_request_diff.persisted? && commits_count.to_i > 0 1797 end 1798 1799 def has_no_commits? 1800 !has_commits? 1801 end 1802 1803 def pipeline_coverage_delta 1804 if base_pipeline&.coverage && head_pipeline&.coverage 1805 head_pipeline.coverage - base_pipeline.coverage 1806 end 1807 end 1808 1809 def use_merge_base_pipeline_for_comparison?(service_class) 1810 ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON[service_class]&.call(project) 1811 end 1812 1813 def comparison_base_pipeline(service_class) 1814 (use_merge_base_pipeline_for_comparison?(service_class) && merge_base_pipeline) || base_pipeline 1815 end 1816 1817 def base_pipeline 1818 @base_pipeline ||= project.ci_pipelines 1819 .order(id: :desc) 1820 .find_by(sha: diff_base_sha, ref: target_branch) 1821 end 1822 1823 def merge_base_pipeline 1824 @merge_base_pipeline ||= project.ci_pipelines 1825 .order(id: :desc) 1826 .find_by(sha: actual_head_pipeline.target_sha, ref: target_branch) 1827 end 1828 1829 def discussions_rendered_on_frontend? 1830 true 1831 end 1832 1833 # rubocop: disable CodeReuse/ServiceClass 1834 def update_project_counter_caches 1835 Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache 1836 end 1837 # rubocop: enable CodeReuse/ServiceClass 1838 1839 def first_contribution? 1840 return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST 1841 1842 !project.merge_requests.merged.exists?(author_id: author_id) 1843 end 1844 1845 # TODO: remove once production database rename completes 1846 # https://gitlab.com/gitlab-org/gitlab-foss/issues/47592 1847 alias_attribute :allow_collaboration, :allow_maintainer_to_push 1848 1849 def allow_collaboration 1850 collaborative_push_possible? && allow_maintainer_to_push 1851 end 1852 1853 alias_method :allow_collaboration?, :allow_collaboration 1854 1855 def collaborative_push_possible? 1856 source_project.present? && for_fork? && 1857 target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && 1858 source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && 1859 !ProtectedBranch.protected?(source_project, source_branch) 1860 end 1861 1862 def can_allow_collaboration?(user) 1863 collaborative_push_possible? && 1864 Ability.allowed?(user, :push_code, source_project) 1865 end 1866 1867 def find_actual_head_pipeline 1868 ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do 1869 all_pipelines.for_sha_or_source_sha(diff_head_sha).first 1870 end 1871 end 1872 1873 def etag_caching_enabled? 1874 true 1875 end 1876 1877 def recent_visible_deployments 1878 deployments.visible.includes(:environment).order(id: :desc).limit(10) 1879 end 1880 1881 def banzai_render_context(field) 1882 super.merge(label_url_method: :project_merge_requests_url) 1883 end 1884 1885 override :ensure_metrics 1886 def ensure_metrics 1887 MergeRequest::Metrics.record!(self) 1888 end 1889 1890 def allows_reviewers? 1891 true 1892 end 1893 1894 def allows_multiple_reviewers? 1895 false 1896 end 1897 1898 def supports_assignee? 1899 true 1900 end 1901 1902 def find_assignee(user) 1903 merge_request_assignees.find_by(user_id: user.id) 1904 end 1905 1906 def find_reviewer(user) 1907 merge_request_reviewers.find_by(user_id: user.id) 1908 end 1909 1910 def enabled_reports 1911 { 1912 sast: report_type_enabled?(:sast), 1913 secret_detection: report_type_enabled?(:secret_detection) 1914 } 1915 end 1916 1917 def includes_ci_config? 1918 return false unless diff_stats 1919 1920 diff_stats.map(&:path).include?(project.ci_config_path_or_default) 1921 end 1922 1923 def context_commits_diff 1924 strong_memoize(:context_commits_diff) do 1925 ContextCommitsDiff.new(self) 1926 end 1927 end 1928 1929 def attention_requested_enabled? 1930 Feature.enabled?(:mr_attention_requests, project, default_enabled: :yaml) 1931 end 1932 1933 private 1934 1935 def set_draft_status 1936 self.draft = draft? 1937 end 1938 1939 def missing_report_error(report_type) 1940 { status: :error, status_reason: "This merge request does not have #{report_type} reports" } 1941 end 1942 1943 def with_rebase_lock 1944 with_retried_nowait_lock { yield } 1945 end 1946 1947 # If the merge request is idle in transaction or has a SELECT FOR 1948 # UPDATE, we don't want to block indefinitely or this could cause a 1949 # queue of SELECT FOR UPDATE calls. Instead, try to get the lock for 1950 # 5 s before raising an error to the user. 1951 def with_retried_nowait_lock 1952 # Try at most 0.25 + (1.5 * .25) + (1.5^2 * .25) ... (1.5^5 * .25) = 5.2 s to get the lock 1953 Retriable.retriable(on: ActiveRecord::LockWaitTimeout, tries: 6, base_interval: 0.25) do 1954 with_lock('FOR UPDATE NOWAIT') do 1955 yield 1956 end 1957 end 1958 rescue ActiveRecord::LockWaitTimeout => e 1959 Gitlab::ErrorTracking.track_exception(e) 1960 raise RebaseLockTimeout, _('Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later.') 1961 end 1962 1963 def source_project_variables 1964 Gitlab::Ci::Variables::Collection.new.tap do |variables| 1965 break variables unless source_project 1966 1967 variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID', value: source_project.id.to_s) 1968 variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH', value: source_project.full_path) 1969 variables.append(key: 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', value: source_project.web_url) 1970 variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', value: source_branch.to_s) 1971 end 1972 end 1973 1974 def expire_etag_cache 1975 return unless project.namespace 1976 1977 key = Gitlab::Routing.url_helpers.cached_widget_project_json_merge_request_path(project, self, format: :json) 1978 Gitlab::EtagCaching::Store.new.touch(key) 1979 end 1980 1981 def report_type_enabled?(report_type) 1982 !!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type) 1983 end 1984end 1985 1986MergeRequest.prepend_mod_with('MergeRequest') 1987