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 SaveDesignsService < DesignService
include RunsDesignActions
include OnSuccessCallbacks
MAX_FILES = 10
def initialize(project, user, params = {})
super
@files = params.fetch(:files)
end
def execute
return error("Not allowed!") unless can_create_designs?
return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES
return error("Duplicate filenames are not allowed!") if files.map(&:original_filename).uniq.length != files.length
return error("Design copy is in progress") if design_collection.copy_in_progress?
uploaded_designs, version = upload_designs!
skipped_designs = designs - uploaded_designs
create_events
design_collection.reset_copy!
success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs })
rescue ::ActiveRecord::RecordInvalid => e
error(e.message)
end
private
attr_reader :files
def upload_designs!
::DesignManagement::Version.with_lock(project.id, repository) do
actions = build_actions
[
actions.map(&:design),
actions.presence && run_actions(actions)
]
end
end
# Returns `Design` instances that correspond with `files`.
# New `Design`s will be created where a file name does not match
# an existing `Design`
def designs
@designs ||= files.map do |file|
collection.find_or_create_design!(filename: file.original_filename)
end
end
def build_actions
@actions ||= files.zip(designs).flat_map do |(file, design)|
Array.wrap(build_design_action(file, design))
end
end
def build_design_action(file, design)
content = file_content(file, design.full_path)
return if design_unchanged?(design, content)
action = new_file?(design) ? :create : :update
on_success do
track_usage_metrics(action)
end
DesignManagement::DesignAction.new(design, action, content)
end
# Returns true if the design file is the same as its latest version
def design_unchanged?(design, content)
content == existing_blobs[design]&.data
end
def create_events
by_action = @actions.group_by(&:action).transform_values { |grp| grp.map(&:design) }
event_create_service.save_designs(current_user, **by_action)
end
def event_create_service
@event_create_service ||= EventCreateService.new
end
def commit_message
<<~MSG
Updated #{files.size} #{'designs'.pluralize(files.size)}
#{formatted_file_list}
MSG
end
def formatted_file_list
filenames.map { |name| "- #{name}" }.join("\n")
end
def filenames
@filenames ||= files.map(&:original_filename)
end
def can_create_designs?
Ability.allowed?(current_user, :create_design, issue)
end
def new_file?(design)
!existing_blobs[design]
end
def file_content(file, full_path)
transformer = ::Lfs::FileTransformer.new(project, repository, target_branch)
transformer.new_file(full_path, file.to_io).content
end
# Returns the latest blobs for the designs as a Hash of `{ Design => Blob }`
def existing_blobs
@existing_blobs ||= begin
items = designs.map { |d| ['HEAD', d.full_path] }
repository.blobs_at(items).each_with_object({}) do |blob, h|
design = designs.find { |d| d.full_path == blob.path }
h[design] = blob
end
end
end
def track_usage_metrics(action)
if action == :update
::Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_modified_action(author: current_user)
else
::Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_added_action(author: current_user)
end
::Gitlab::UsageDataCounters::DesignsCounter.count(action)
end
end
end
DesignManagement::SaveDesignsService.prepend_mod_with('DesignManagement::SaveDesignsService')
|