1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
# 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 = sha
@issue_id = issue_id
@actions = 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
validates :issue, 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) }
scope :with_author, -> { includes(:author) }
# 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) }
Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
version
end
rescue StandardError
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
|