summaryrefslogtreecommitdiff
path: root/app/models/design_management/version.rb
blob: ca65cf38f0d09630f19b7da4342aaeb5c5b14854 (plain)
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