2020-05-24 23:13:21 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module DesignManagement
|
|
|
|
class Version < ApplicationRecord
|
|
|
|
include Importable
|
|
|
|
include ShaAttribute
|
|
|
|
include AfterCommitQueue
|
|
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
extend Gitlab::ExclusiveLeaseHelpers
|
|
|
|
|
|
|
|
NotSameIssue = Class.new(StandardError)
|
|
|
|
|
|
|
|
class CouldNotCreateVersion < StandardError
|
|
|
|
attr_reader :sha, :issue_id, :actions
|
|
|
|
|
|
|
|
def initialize(sha, issue_id, actions)
|
|
|
|
@sha, @issue_id, @actions = sha, issue_id, actions
|
|
|
|
end
|
|
|
|
|
|
|
|
def message
|
|
|
|
"could not create version from commit: #{sha}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def sentry_extra_data
|
|
|
|
{
|
|
|
|
sha: sha,
|
|
|
|
issue_id: issue_id,
|
|
|
|
design_ids: actions.map { |a| a.design.id }
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
belongs_to :issue
|
|
|
|
belongs_to :author, class_name: 'User'
|
|
|
|
has_many :actions
|
|
|
|
has_many :designs,
|
|
|
|
through: :actions,
|
|
|
|
class_name: "DesignManagement::Design",
|
|
|
|
source: :design,
|
|
|
|
inverse_of: :versions
|
|
|
|
|
|
|
|
validates :designs, presence: true, unless: :importing?
|
|
|
|
validates :sha, presence: true
|
|
|
|
validates :sha, uniqueness: { case_sensitive: false, scope: :issue_id }
|
|
|
|
validates :author, presence: true
|
|
|
|
# We are not validating the issue object as it incurs an extra query to fetch
|
|
|
|
# the record from the DB. Instead, we rely on the foreign key constraint to
|
|
|
|
# ensure referential integrity.
|
|
|
|
validates :issue_id, presence: true, unless: :importing?
|
|
|
|
|
|
|
|
sha_attribute :sha
|
|
|
|
|
|
|
|
delegate :project, to: :issue
|
|
|
|
|
|
|
|
scope :for_designs, -> (designs) do
|
|
|
|
where(id: ::DesignManagement::Action.where(design_id: designs).select(:version_id)).distinct
|
|
|
|
end
|
|
|
|
scope :earlier_or_equal_to, -> (version) { where("(#{table_name}.id) <= ?", version) } # rubocop:disable GitlabSecurity/SqlInjection
|
|
|
|
scope :ordered, -> { order(id: :desc) }
|
|
|
|
scope :for_issue, -> (issue) { where(issue: issue) }
|
|
|
|
scope :by_sha, -> (sha) { where(sha: sha) }
|
|
|
|
|
|
|
|
# This is the one true way to create a Version.
|
|
|
|
#
|
|
|
|
# This method means you can avoid the paradox of versions being invalid without
|
|
|
|
# designs, and not being able to add designs without a saved version. Also this
|
|
|
|
# method inserts designs in bulk, rather than one by one.
|
|
|
|
#
|
|
|
|
# Before calling this method, callers must guard against concurrent
|
|
|
|
# modification by obtaining the lock on the design repository. See:
|
|
|
|
# `DesignManagement::Version.with_lock`.
|
|
|
|
#
|
|
|
|
# Parameters:
|
|
|
|
# - design_actions [DesignManagement::DesignAction]:
|
|
|
|
# the actions that have been performed in the repository.
|
|
|
|
# - sha [String]:
|
|
|
|
# the SHA of the commit that performed them
|
|
|
|
# - author [User]:
|
|
|
|
# the user who performed the commit
|
|
|
|
# returns [DesignManagement::Version]
|
|
|
|
def self.create_for_designs(design_actions, sha, author)
|
|
|
|
issue_id, not_uniq = design_actions.map(&:issue_id).compact.uniq
|
|
|
|
raise NotSameIssue, 'All designs must belong to the same issue!' if not_uniq
|
|
|
|
|
|
|
|
transaction do
|
|
|
|
version = new(sha: sha, issue_id: issue_id, author: author)
|
|
|
|
version.save(validate: false) # We need it to have an ID. Validate later when designs are present
|
|
|
|
|
|
|
|
rows = design_actions.map { |action| action.row_attrs(version) }
|
|
|
|
|
2020-06-23 00:09:42 +05:30
|
|
|
Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
|
2020-05-24 23:13:21 +05:30
|
|
|
version.designs.reset
|
|
|
|
version.validate!
|
|
|
|
design_actions.each(&:performed)
|
|
|
|
|
|
|
|
version
|
|
|
|
end
|
|
|
|
rescue
|
|
|
|
raise CouldNotCreateVersion.new(sha, issue_id, design_actions)
|
|
|
|
end
|
|
|
|
|
|
|
|
CREATION_TTL = 5.seconds
|
|
|
|
RETRY_DELAY = ->(num) { 0.2.seconds * num**2 }
|
|
|
|
|
|
|
|
def self.with_lock(project_id, repository, &block)
|
|
|
|
key = "with_lock:#{name}:{#{project_id}}"
|
|
|
|
|
|
|
|
in_lock(key, ttl: CREATION_TTL, retries: 5, sleep_sec: RETRY_DELAY) do |_retried|
|
|
|
|
repository.create_if_not_exists
|
|
|
|
yield
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def designs_by_event
|
|
|
|
actions
|
|
|
|
.includes(:design)
|
|
|
|
.group_by(&:event)
|
|
|
|
.transform_values { |group| group.map(&:design) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def author
|
|
|
|
super || (commit_author if persisted?)
|
|
|
|
end
|
|
|
|
|
|
|
|
def diff_refs
|
|
|
|
strong_memoize(:diff_refs) { commit&.diff_refs }
|
|
|
|
end
|
|
|
|
|
|
|
|
def reset
|
|
|
|
%i[diff_refs commit].each { |k| clear_memoization(k) }
|
|
|
|
super
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def commit_author
|
|
|
|
commit&.author
|
|
|
|
end
|
|
|
|
|
|
|
|
def commit
|
|
|
|
strong_memoize(:commit) { issue.project.design_repository.commit(sha) }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|