packs 0.0.5 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +101 -12
- data/bin/packs +10 -0
- data/bin/rubocop +29 -0
- data/bin/tapioca +29 -0
- data/lib/packs/cli.rb +164 -0
- data/lib/packs/code_ownership_post_processor.rb +58 -0
- data/lib/packs/configuration.rb +61 -0
- data/lib/packs/default_user_event_logger.rb +7 -0
- data/lib/packs/logging.rb +37 -0
- data/lib/packs/per_file_processor_interface.rb +18 -0
- data/lib/packs/private/file_move_operation.rb +80 -0
- data/lib/packs/private/interactive_cli/file_selector.rb +26 -0
- data/lib/packs/private/interactive_cli/pack_selector.rb +55 -0
- data/lib/packs/private/interactive_cli/team_selector.rb +58 -0
- data/lib/packs/private/interactive_cli/use_cases/add_dependency.rb +30 -0
- data/lib/packs/private/interactive_cli/use_cases/check.rb +25 -0
- data/lib/packs/private/interactive_cli/use_cases/create.rb +27 -0
- data/lib/packs/private/interactive_cli/use_cases/get_info.rb +37 -0
- data/lib/packs/private/interactive_cli/use_cases/interface.rb +34 -0
- data/lib/packs/private/interactive_cli/use_cases/lint_package_todo_yml_files.rb +25 -0
- data/lib/packs/private/interactive_cli/use_cases/lint_package_yml_files.rb +26 -0
- data/lib/packs/private/interactive_cli/use_cases/make_public.rb +30 -0
- data/lib/packs/private/interactive_cli/use_cases/move.rb +32 -0
- data/lib/packs/private/interactive_cli/use_cases/move_to_parent.rb +31 -0
- data/lib/packs/private/interactive_cli/use_cases/query.rb +51 -0
- data/lib/packs/private/interactive_cli/use_cases/rename.rb +25 -0
- data/lib/packs/private/interactive_cli/use_cases/update.rb +25 -0
- data/lib/packs/private/interactive_cli/use_cases/validate.rb +25 -0
- data/lib/packs/private/interactive_cli/use_cases/visualize.rb +44 -0
- data/lib/packs/private/interactive_cli.rb +52 -0
- data/lib/packs/private/pack_relationship_analyzer.rb +135 -0
- data/lib/packs/private/packwerk_wrapper/offenses_aggregator_formatter.rb +44 -0
- data/lib/packs/private/packwerk_wrapper.rb +70 -0
- data/lib/packs/private.rb +606 -4
- data/lib/packs/rubocop_post_processor.rb +30 -0
- data/lib/packs/user_event_logger.rb +199 -0
- data/lib/packs.rb +233 -53
- metadata +225 -14
- data/lib/packs/pack.rb +0 -43
- data/lib/packs/private/configuration.rb +0 -36
- data/lib/packs/rspec/fixture_helper.rb +0 -33
- data/lib/packs/rspec/support.rb +0 -21
data/lib/packs/private.rb
CHANGED
@@ -1,14 +1,616 @@
|
|
1
1
|
# typed: strict
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'pathname'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'colorized_string'
|
6
|
+
require 'sorbet-runtime'
|
7
|
+
|
8
|
+
require 'packs/private/file_move_operation'
|
9
|
+
require 'packs/private/pack_relationship_analyzer'
|
10
|
+
require 'packs/private/interactive_cli'
|
11
|
+
require 'packs/private/packwerk_wrapper'
|
4
12
|
|
5
13
|
module Packs
|
6
14
|
module Private
|
7
15
|
extend T::Sig
|
8
16
|
|
9
|
-
sig { returns(
|
10
|
-
def self.
|
11
|
-
|
17
|
+
sig { params(pack_name: String).returns(String) }
|
18
|
+
def self.clean_pack_name(pack_name)
|
19
|
+
# The reason we do this is a lot of terminals add an extra `/` when you tab-autocomplete.
|
20
|
+
# This results in the pack not being found, but when we write the package YML it writes to the same place,
|
21
|
+
# causing a behaviorally confusing diff.
|
22
|
+
# We ignore trailing slashes as an ergonomic feature to the user.
|
23
|
+
pack_name.gsub(%r{/$}, '')
|
24
|
+
end
|
25
|
+
|
26
|
+
sig do
|
27
|
+
params(
|
28
|
+
file: String,
|
29
|
+
find: Pathname,
|
30
|
+
replace_with: Pathname
|
31
|
+
).void
|
32
|
+
end
|
33
|
+
def self.replace_in_file(file:, find:, replace_with:)
|
34
|
+
file = Pathname.new(file)
|
35
|
+
return if !file.exist?
|
36
|
+
|
37
|
+
count = 0
|
38
|
+
file.write(file.read.gsub(find.to_s) do
|
39
|
+
count += 1
|
40
|
+
replace_with.to_s
|
41
|
+
end)
|
42
|
+
Logging.print "Replaced #{count} occurrence(s) of #{find} in #{file}" if count > 0
|
43
|
+
end
|
44
|
+
|
45
|
+
sig do
|
46
|
+
params(
|
47
|
+
pack_name: String,
|
48
|
+
enforce_privacy: T::Boolean,
|
49
|
+
enforce_dependencies: T.nilable(T::Boolean),
|
50
|
+
team: T.nilable(CodeTeams::Team)
|
51
|
+
).void
|
52
|
+
end
|
53
|
+
def self.create_pack!(pack_name:, enforce_privacy:, enforce_dependencies:, team:)
|
54
|
+
Logging.section('👋 Hi!') do
|
55
|
+
intro = Packs.config.user_event_logger.before_create_pack(pack_name)
|
56
|
+
Logging.print_bold_green(intro)
|
57
|
+
end
|
58
|
+
|
59
|
+
pack_name = Private.clean_pack_name(pack_name)
|
60
|
+
|
61
|
+
package = create_pack_if_not_exists!(pack_name: pack_name, enforce_privacy: enforce_privacy, enforce_dependencies: enforce_dependencies, team: team)
|
62
|
+
add_public_directory(package)
|
63
|
+
add_readme_todo(package)
|
64
|
+
|
65
|
+
Logging.section('Next steps') do
|
66
|
+
next_steps = Packs.config.user_event_logger.after_create_pack(pack_name)
|
67
|
+
|
68
|
+
Logging.print_bold_green(next_steps)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
sig do
|
73
|
+
params(
|
74
|
+
pack_name: String,
|
75
|
+
paths_relative_to_root: T::Array[String],
|
76
|
+
per_file_processors: T::Array[Packs::PerFileProcessorInterface]
|
77
|
+
).void
|
78
|
+
end
|
79
|
+
def self.move_to_pack!(pack_name:, paths_relative_to_root:, per_file_processors: [])
|
80
|
+
pack_name = Private.clean_pack_name(pack_name)
|
81
|
+
package = ParsePackwerk.all.find { |p| p.name == pack_name }
|
82
|
+
if package.nil?
|
83
|
+
raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
|
84
|
+
end
|
85
|
+
|
86
|
+
add_public_directory(package)
|
87
|
+
add_readme_todo(package)
|
88
|
+
package_location = package.directory
|
89
|
+
|
90
|
+
file_move_operations = T.let([], T::Array[Private::FileMoveOperation])
|
91
|
+
|
92
|
+
if paths_relative_to_root.any?
|
93
|
+
Logging.section('File Operations') do
|
94
|
+
file_paths = paths_relative_to_root.flat_map do |path|
|
95
|
+
origin_pathname = Pathname.new(path).cleanpath
|
96
|
+
# Note -- we used to `mv` over whole directories, rather than splatting out their contents and merging individual files.
|
97
|
+
# The main advantage to moving whole directories is that it's a bit faster and a bit less verbose
|
98
|
+
# However, this ended up being tricky and caused complexity to flow down later parts of the implementation.
|
99
|
+
# Notably:
|
100
|
+
# 1) The `mv` operation doesn't merge directories, so if the destination already has the same directory, then the mv operation
|
101
|
+
# will overwrite
|
102
|
+
# 2) We could get around this possibly with `cp_r` (https://ruby-doc.org/stdlib-1.9.3/libdoc/fileutils/rdoc/FileUtils.html#method-c-cp_r),
|
103
|
+
# but we'd also have to delete the origin destination. On top of this, we still need to splat things out later on so that we can do
|
104
|
+
# per file processor operations, and that has some complexity of its own. The simplest thing here would be to simply glob everything out.
|
105
|
+
#
|
106
|
+
# For now, we sacrifice some small level of speed and conciseness in favor of simpler implementation.
|
107
|
+
# Later, if we choose to go back to moving whole directories at a time, it should be a refactor and all tests should still pass
|
108
|
+
#
|
109
|
+
if origin_pathname.directory?
|
110
|
+
origin_pathname.glob('**/*.*').reject do |origin_path|
|
111
|
+
origin_path.to_s.include?(ParsePackwerk::PACKAGE_YML_NAME) ||
|
112
|
+
origin_path.to_s.include?(ParsePackwerk::PACKAGE_TODO_YML_NAME)
|
113
|
+
end
|
114
|
+
else
|
115
|
+
origin_pathname
|
116
|
+
end
|
117
|
+
end
|
118
|
+
file_move_operations = file_paths.flat_map do |origin_pathname|
|
119
|
+
file_move_operation = FileMoveOperation.new(
|
120
|
+
origin_pathname: origin_pathname,
|
121
|
+
destination_pathname: FileMoveOperation.destination_pathname_for_package_move(origin_pathname, package_location),
|
122
|
+
destination_pack: package
|
123
|
+
)
|
124
|
+
[
|
125
|
+
file_move_operation,
|
126
|
+
file_move_operation.spec_file_move_operation
|
127
|
+
]
|
128
|
+
end
|
129
|
+
file_move_operations.each do |file_move_operation|
|
130
|
+
Private.package_filepath(file_move_operation, per_file_processors)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
per_file_processors.each do |processor|
|
136
|
+
processor.after_move_files!(file_move_operations)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
sig do
|
141
|
+
params(
|
142
|
+
pack_name: String,
|
143
|
+
parent_name: String,
|
144
|
+
per_file_processors: T::Array[PerFileProcessorInterface]
|
145
|
+
).void
|
146
|
+
end
|
147
|
+
def self.move_to_parent!(
|
148
|
+
pack_name:,
|
149
|
+
parent_name:,
|
150
|
+
per_file_processors: []
|
151
|
+
)
|
152
|
+
pack_name = Private.clean_pack_name(pack_name)
|
153
|
+
package = ParsePackwerk.all.find { |p| p.name == pack_name }
|
154
|
+
if package.nil?
|
155
|
+
raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
|
156
|
+
end
|
157
|
+
|
158
|
+
parent_name = Private.clean_pack_name(parent_name)
|
159
|
+
parent_package = ParsePackwerk.all.find { |p| p.name == parent_name }
|
160
|
+
if parent_package.nil?
|
161
|
+
parent_package = create_pack_if_not_exists!(pack_name: parent_name, enforce_privacy: true, enforce_dependencies: true)
|
162
|
+
end
|
163
|
+
|
164
|
+
# First we create a new pack that has the exact same properties of the old one!
|
165
|
+
package_last_name = package.directory.basename
|
166
|
+
new_package_name = parent_package.directory.join(package_last_name).to_s
|
167
|
+
|
168
|
+
new_package = ParsePackwerk::Package.new(
|
169
|
+
name: new_package_name,
|
170
|
+
enforce_privacy: package.enforce_dependencies,
|
171
|
+
enforce_dependencies: package.enforce_dependencies,
|
172
|
+
dependencies: package.dependencies,
|
173
|
+
metadata: package.metadata,
|
174
|
+
config: package.config
|
175
|
+
)
|
176
|
+
ParsePackwerk.write_package_yml!(new_package)
|
177
|
+
ParsePackwerk.bust_cache!
|
178
|
+
|
179
|
+
# Move everything from the old pack to the new one
|
180
|
+
move_to_pack!(
|
181
|
+
pack_name: new_package_name,
|
182
|
+
paths_relative_to_root: [package.directory.to_s],
|
183
|
+
per_file_processors: per_file_processors
|
184
|
+
)
|
185
|
+
|
186
|
+
# Then delete the old package.yml and package_todo.yml files
|
187
|
+
package.yml.delete
|
188
|
+
package_todo_file = ParsePackwerk::PackageTodo.for(package).pathname
|
189
|
+
package_todo_file.delete if package_todo_file.exist?
|
190
|
+
|
191
|
+
ParsePackwerk.bust_cache!
|
192
|
+
|
193
|
+
ParsePackwerk.all.each do |other_package|
|
194
|
+
new_dependencies = other_package.dependencies.map { |d| d == pack_name ? new_package_name : d }
|
195
|
+
if other_package.name == parent_name && !new_dependencies.include?(new_package_name)
|
196
|
+
new_dependencies += [new_package_name]
|
197
|
+
end
|
198
|
+
|
199
|
+
new_other_package = ParsePackwerk::Package.new(
|
200
|
+
name: other_package.name,
|
201
|
+
enforce_privacy: other_package.enforce_privacy,
|
202
|
+
enforce_dependencies: other_package.enforce_dependencies,
|
203
|
+
dependencies: new_dependencies.uniq.sort,
|
204
|
+
metadata: other_package.metadata,
|
205
|
+
config: other_package.config
|
206
|
+
)
|
207
|
+
|
208
|
+
ParsePackwerk.write_package_yml!(new_other_package)
|
209
|
+
end
|
210
|
+
|
211
|
+
sorbet_config = Pathname.new('sorbet/config')
|
212
|
+
if sorbet_config.exist?
|
213
|
+
Packs.replace_in_file(
|
214
|
+
file: sorbet_config.to_s,
|
215
|
+
find: package.directory.join('spec'),
|
216
|
+
replace_with: new_package.directory.join('spec')
|
217
|
+
)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
sig do
|
222
|
+
params(
|
223
|
+
paths_relative_to_root: T::Array[String],
|
224
|
+
per_file_processors: T::Array[Packs::PerFileProcessorInterface]
|
225
|
+
).void
|
226
|
+
end
|
227
|
+
def self.make_public!(paths_relative_to_root:, per_file_processors:)
|
228
|
+
if paths_relative_to_root.any?
|
229
|
+
file_move_operations = T.let([], T::Array[Private::FileMoveOperation])
|
230
|
+
|
231
|
+
Logging.section('File Operations') do
|
232
|
+
file_paths = paths_relative_to_root.flat_map do |path|
|
233
|
+
origin_pathname = Pathname.new(path).cleanpath
|
234
|
+
if origin_pathname.directory?
|
235
|
+
origin_pathname.glob('**/*.*').map(&:to_s)
|
236
|
+
else
|
237
|
+
path
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
file_move_operations = file_paths.flat_map do |path|
|
242
|
+
package = ParsePackwerk.package_from_path(path)
|
243
|
+
origin_pathname = Pathname.new(path).cleanpath
|
244
|
+
|
245
|
+
file_move_operation = FileMoveOperation.new(
|
246
|
+
origin_pathname: origin_pathname,
|
247
|
+
destination_pathname: FileMoveOperation.destination_pathname_for_new_public_api(origin_pathname),
|
248
|
+
destination_pack: package
|
249
|
+
)
|
250
|
+
|
251
|
+
[
|
252
|
+
file_move_operation,
|
253
|
+
file_move_operation.spec_file_move_operation
|
254
|
+
]
|
255
|
+
end
|
256
|
+
|
257
|
+
file_move_operations.each do |file_move_operation|
|
258
|
+
Private.package_filepath(file_move_operation, per_file_processors)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
per_file_processors.each do |processor|
|
263
|
+
processor.after_move_files!(file_move_operations)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
sig do
|
269
|
+
params(
|
270
|
+
pack_name: String,
|
271
|
+
dependency_name: String
|
272
|
+
).void
|
273
|
+
end
|
274
|
+
def self.add_dependency!(pack_name:, dependency_name:)
|
275
|
+
all_packages = ParsePackwerk.all
|
276
|
+
|
277
|
+
pack_name = Private.clean_pack_name(pack_name)
|
278
|
+
package = all_packages.find { |p| p.name == pack_name }
|
279
|
+
if package.nil?
|
280
|
+
raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
|
281
|
+
end
|
282
|
+
|
283
|
+
dependency_name = Private.clean_pack_name(dependency_name)
|
284
|
+
package_dependency = all_packages.find { |p| p.name == dependency_name }
|
285
|
+
if package_dependency.nil?
|
286
|
+
raise StandardError, "Can not find package with name #{dependency_name}. Make sure the argument is of the form `packs/my_pack/`"
|
287
|
+
end
|
288
|
+
|
289
|
+
new_package = ParsePackwerk::Package.new(
|
290
|
+
name: pack_name,
|
291
|
+
dependencies: (package.dependencies + [dependency_name]).uniq.sort,
|
292
|
+
enforce_privacy: package.enforce_privacy,
|
293
|
+
enforce_dependencies: package.enforce_dependencies,
|
294
|
+
metadata: package.metadata,
|
295
|
+
config: package.config
|
296
|
+
)
|
297
|
+
ParsePackwerk.write_package_yml!(new_package)
|
298
|
+
PackwerkWrapper.validate!
|
299
|
+
end
|
300
|
+
|
301
|
+
sig { params(file_move_operation: FileMoveOperation, per_file_processors: T::Array[Packs::PerFileProcessorInterface]).void }
|
302
|
+
def self.package_filepath(file_move_operation, per_file_processors)
|
303
|
+
per_file_processors.each do |per_file_processor|
|
304
|
+
if file_move_operation.origin_pathname.exist?
|
305
|
+
per_file_processor.before_move_file!(file_move_operation)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
origin = file_move_operation.origin_pathname
|
310
|
+
destination = file_move_operation.destination_pathname
|
311
|
+
idempotent_mv(origin, destination)
|
312
|
+
end
|
313
|
+
|
314
|
+
sig { params(origin: Pathname, destination: Pathname).void }
|
315
|
+
def self.idempotent_mv(origin, destination)
|
316
|
+
if origin.exist? && destination.exist?
|
317
|
+
Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, #{destination} already exists").red
|
318
|
+
elsif origin.exist? && !destination.exist?
|
319
|
+
destination.dirname.mkpath
|
320
|
+
|
321
|
+
Logging.print "Moving file #{origin} to #{destination}"
|
322
|
+
# use git mv so that git knows that it was a move
|
323
|
+
FileUtils.mv(origin, destination)
|
324
|
+
elsif !origin.exist? && destination.exist?
|
325
|
+
Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, does not exist, (#{destination} already exists)").red
|
326
|
+
else
|
327
|
+
# We could choose to print this in a `--verbose` mode. For now, we find that printing this text in red confuses folks more than it informs them.
|
328
|
+
# This is because it's perfectly common for a spec to not exist for a file, so at best it's a warning.
|
329
|
+
# Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, does not exist").red
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
sig { params(package: ParsePackwerk::Package).void }
|
334
|
+
def self.add_public_directory(package)
|
335
|
+
public_directory = package.directory.join('app/public')
|
336
|
+
|
337
|
+
if public_directory.glob('**/**.rb').none?
|
338
|
+
FileUtils.mkdir_p(public_directory)
|
339
|
+
todo_md = Packs.config.user_event_logger.on_create_public_directory_todo(package.name)
|
340
|
+
public_directory.join('TODO.md').write(todo_md)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
sig { params(package: ParsePackwerk::Package).void }
|
345
|
+
def self.add_readme_todo(package)
|
346
|
+
pack_directory = package.directory
|
347
|
+
|
348
|
+
if !pack_directory.join('README.md').exist?
|
349
|
+
readme_todo_md = Packs.config.user_event_logger.on_create_readme_todo(package.name)
|
350
|
+
pack_directory.join('README_TODO.md').write(readme_todo_md)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
sig do
|
355
|
+
params(
|
356
|
+
pack_name: String,
|
357
|
+
enforce_privacy: T::Boolean,
|
358
|
+
enforce_dependencies: T.nilable(T::Boolean),
|
359
|
+
team: T.nilable(CodeTeams::Team)
|
360
|
+
).returns(ParsePackwerk::Package)
|
361
|
+
end
|
362
|
+
def self.create_pack_if_not_exists!(pack_name:, enforce_privacy:, enforce_dependencies:, team: nil)
|
363
|
+
if PERMITTED_PACK_LOCATIONS.none? { |permitted_location| pack_name.start_with?(permitted_location) }
|
364
|
+
raise StandardError, "Packs only supports packages in the the following directories: #{PERMITTED_PACK_LOCATIONS.inspect}. Please make sure to pass in the name of the pack including the full directory path, e.g. `packs/my_pack`."
|
365
|
+
end
|
366
|
+
|
367
|
+
existing_package = ParsePackwerk.all.find { |p| p.name == pack_name }
|
368
|
+
if existing_package.nil?
|
369
|
+
should_enforce_dependencies = enforce_dependencies.nil? ? Packs.config.enforce_dependencies : enforce_dependencies
|
370
|
+
|
371
|
+
# TODO: This should probably be `if defined?(CodeOwnership) && CodeOwnership.configured?`
|
372
|
+
# but we'll need to add an API to CodeOwnership to do this
|
373
|
+
if Pathname.new('config/code_ownership.yml').exist?
|
374
|
+
config = {
|
375
|
+
'owner' => team.nil? ? 'MyTeam' : team.name
|
376
|
+
}
|
377
|
+
else
|
378
|
+
config = {}
|
379
|
+
end
|
380
|
+
|
381
|
+
package = ParsePackwerk::Package.new(
|
382
|
+
enforce_dependencies: should_enforce_dependencies || false,
|
383
|
+
enforce_privacy: enforce_privacy,
|
384
|
+
dependencies: [],
|
385
|
+
metadata: {},
|
386
|
+
name: pack_name,
|
387
|
+
config: config
|
388
|
+
)
|
389
|
+
|
390
|
+
ParsePackwerk.write_package_yml!(package)
|
391
|
+
|
392
|
+
current_contents = package.yml.read
|
393
|
+
new_contents = current_contents.gsub('MyTeam', 'MyTeam # specify your team here, or delete this key if this package is not owned by one team')
|
394
|
+
package.yml.write(new_contents)
|
395
|
+
existing_package = package
|
396
|
+
end
|
397
|
+
|
398
|
+
existing_package
|
399
|
+
end
|
400
|
+
|
401
|
+
sig { void }
|
402
|
+
def self.load_client_configuration
|
403
|
+
@loaded_client_configuration ||= T.let(false, T.nilable(T::Boolean))
|
404
|
+
return if @loaded_client_configuration
|
405
|
+
|
406
|
+
@loaded_client_configuration = true
|
407
|
+
client_configuration = Pathname.pwd.join('config/use_packs.rb')
|
408
|
+
require client_configuration.to_s if client_configuration.exist?
|
409
|
+
end
|
410
|
+
|
411
|
+
sig { void }
|
412
|
+
def self.bust_cache!
|
413
|
+
Packs.config.bust_cache!
|
414
|
+
# This comes explicitly after `Packs.config.bust_cache!` because
|
415
|
+
# otherwise `Packs.config` will attempt to reload the client configuratoin.
|
416
|
+
@loaded_client_configuration = false
|
417
|
+
end
|
418
|
+
|
419
|
+
sig { returns(T::Hash[String, String]) }
|
420
|
+
def self.get_package_todo_contents
|
421
|
+
package_todo = {}
|
422
|
+
ParsePackwerk.all.each do |package|
|
423
|
+
package_todo_yml = ParsePackwerk::PackageTodo.for(package).pathname
|
424
|
+
if package_todo_yml.exist?
|
425
|
+
package_todo[package_todo_yml.to_s] = package_todo_yml.read
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
package_todo
|
430
|
+
end
|
431
|
+
|
432
|
+
PackageTodoFiles = T.type_alias do
|
433
|
+
T::Hash[String, T.nilable(String)]
|
434
|
+
end
|
435
|
+
|
436
|
+
sig { params(before: PackageTodoFiles, after: PackageTodoFiles).returns(String) }
|
437
|
+
def self.diff_package_todo_yml(before, after)
|
438
|
+
dir_containing_contents_before = Dir.mktmpdir
|
439
|
+
dir_containing_contents_after = Dir.mktmpdir
|
440
|
+
begin
|
441
|
+
write_package_todo_to_tmp_folder(before, dir_containing_contents_before)
|
442
|
+
write_package_todo_to_tmp_folder(after, dir_containing_contents_after)
|
443
|
+
|
444
|
+
diff = `diff -r #{dir_containing_contents_before}/ #{dir_containing_contents_after}/`
|
445
|
+
# For ease of reading, sub out the tmp directory from the diff
|
446
|
+
diff.gsub(dir_containing_contents_before, '').gsub(dir_containing_contents_after, '')
|
447
|
+
ensure
|
448
|
+
FileUtils.remove_entry dir_containing_contents_before
|
449
|
+
FileUtils.remove_entry dir_containing_contents_after
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
sig { params(package_todo_files: PackageTodoFiles, tmp_folder: String).void }
|
454
|
+
def self.write_package_todo_to_tmp_folder(package_todo_files, tmp_folder)
|
455
|
+
package_todo_files.each do |filename, contents|
|
456
|
+
next if contents.nil?
|
457
|
+
|
458
|
+
tmp_folder_pathname = Pathname.new(tmp_folder)
|
459
|
+
temp_package_todo_yml = tmp_folder_pathname.join(filename)
|
460
|
+
FileUtils.mkdir_p(temp_package_todo_yml.dirname)
|
461
|
+
temp_package_todo_yml.write(contents)
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
sig { params(packages: T::Array[ParsePackwerk::Package]).returns(T::Array[Packs::Pack]) }
|
466
|
+
def self.packwerk_packages_to_packs(packages)
|
467
|
+
packs = []
|
468
|
+
packages.each do |package|
|
469
|
+
pack = Packs.find(package.name)
|
470
|
+
packs << pack if !pack.nil?
|
471
|
+
end
|
472
|
+
|
473
|
+
packs
|
474
|
+
end
|
475
|
+
|
476
|
+
sig { params(package: ParsePackwerk::Package).returns(T.nilable(Packs::Pack)) }
|
477
|
+
def self.packwerk_package_to_pack(package)
|
478
|
+
Packs.find(package.name)
|
479
|
+
end
|
480
|
+
|
481
|
+
sig { params(packs: T::Array[Packs::Pack]).void }
|
482
|
+
def self.get_info(packs: Packs.all)
|
483
|
+
inbound_violations = {}
|
484
|
+
outbound_violations = {}
|
485
|
+
ParsePackwerk.all.each do |p|
|
486
|
+
p.violations.each do |violation|
|
487
|
+
outbound_violations[p.name] ||= []
|
488
|
+
outbound_violations[p.name] << violation
|
489
|
+
inbound_violations[violation.to_package_name] ||= []
|
490
|
+
inbound_violations[violation.to_package_name] << violation
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
all_inbound = T.let([], T::Array[ParsePackwerk::Violation])
|
495
|
+
all_outbound = T.let([], T::Array[ParsePackwerk::Violation])
|
496
|
+
packs.each do |pack|
|
497
|
+
all_inbound += inbound_violations[pack.name] || []
|
498
|
+
all_outbound += outbound_violations[pack.name] || []
|
499
|
+
end
|
500
|
+
|
501
|
+
puts "There are #{all_inbound.select(&:privacy?).sum { |v| v.files.count }} total inbound privacy violations"
|
502
|
+
puts "There are #{all_inbound.select(&:dependency?).sum { |v| v.files.count }} total inbound dependency violations"
|
503
|
+
puts "There are #{all_outbound.select(&:privacy?).sum { |v| v.files.count }} total outbound privacy violations"
|
504
|
+
puts "There are #{all_outbound.select(&:dependency?).sum { |v| v.files.count }} total outbound dependency violations"
|
505
|
+
|
506
|
+
packs.sort_by { |p| -p.relative_path.glob('**/*.rb').count }.each do |pack|
|
507
|
+
puts "\n=========== Info about: #{pack.name}"
|
508
|
+
|
509
|
+
owner = CodeOwnership.for_package(pack)
|
510
|
+
puts "Owned by: #{owner.nil? ? 'No one' : owner.name}"
|
511
|
+
puts "Size: #{pack.relative_path.glob('**/*.rb').count} ruby files"
|
512
|
+
puts "Public API: #{pack.relative_path.join('app/public')}"
|
513
|
+
|
514
|
+
inbound_for_pack = inbound_violations[pack.name] || []
|
515
|
+
outbound_for_pack = outbound_violations[pack.name] || []
|
516
|
+
puts "There are #{inbound_for_pack.select(&:privacy?).sum { |v| v.files.count }} inbound privacy violations"
|
517
|
+
puts "There are #{inbound_for_pack.flatten.select(&:dependency?).sum { |v| v.files.count }} inbound dependency violations"
|
518
|
+
puts "There are #{outbound_for_pack.select(&:privacy?).sum { |v| v.files.count }} outbound privacy violations"
|
519
|
+
puts "There are #{outbound_for_pack.flatten.select(&:dependency?).sum { |v| v.files.count }} outbound dependency violations"
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
sig { void }
|
524
|
+
def self.lint_package_todo_yml_files!
|
525
|
+
contents_before = Private.get_package_todo_contents
|
526
|
+
Packs.execute(['update-todo'])
|
527
|
+
contents_after = Private.get_package_todo_contents
|
528
|
+
diff = Private.diff_package_todo_yml(contents_before, contents_after)
|
529
|
+
|
530
|
+
if diff == ''
|
531
|
+
# No diff generated by `update-todo`
|
532
|
+
safe_exit 0
|
533
|
+
else
|
534
|
+
output = <<~OUTPUT
|
535
|
+
All `package_todo.yml` files must be up-to-date and that no diff is generated when running `bin/packwerk update-todo`.
|
536
|
+
This helps ensure a high quality signal in other engineers' PRs when inspecting new violations by ensuring there are no unrelated changes.
|
537
|
+
|
538
|
+
There are three main reasons there may be a diff:
|
539
|
+
1) Most likely, you may have stale violations, meaning there are old violations that no longer apply.
|
540
|
+
2) You may have some sort of auto-formatter set up somewhere (e.g. something that reformats YML files) that is, for example, changing double quotes to single quotes. Ensure this is turned off for these auto-generated files.
|
541
|
+
3) You may have edited these files manually. It's recommended to use the `bin/packwerk update-todo` command to make changes to `package_todo.yml` files.
|
542
|
+
|
543
|
+
In all cases, you can run `bin/packwerk update-todo` to update these files.
|
544
|
+
|
545
|
+
Here is the diff generated after running `update-todo`:
|
546
|
+
```
|
547
|
+
#{diff}
|
548
|
+
```
|
549
|
+
|
550
|
+
OUTPUT
|
551
|
+
|
552
|
+
puts output
|
553
|
+
Packs.config.on_package_todo_lint_failure.call(output)
|
554
|
+
safe_exit 1
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
sig { params(packs: T::Array[Packs::Pack]).void }
|
559
|
+
def self.lint_package_yml_files!(packs)
|
560
|
+
packs.each do |p|
|
561
|
+
packwerk_package = ParsePackwerk.find(p.name)
|
562
|
+
next if packwerk_package.nil?
|
563
|
+
|
564
|
+
new_metadata = packwerk_package.metadata
|
565
|
+
new_config = packwerk_package.config
|
566
|
+
|
567
|
+
# Move metadata owner key to top-level
|
568
|
+
existing_owner = new_config['owner'] || new_metadata.delete('owner')
|
569
|
+
new_config['owner'] = existing_owner if !existing_owner.nil?
|
570
|
+
|
571
|
+
if new_metadata.empty?
|
572
|
+
new_config.delete('metadata')
|
573
|
+
end
|
574
|
+
|
575
|
+
new_package = packwerk_package.with(
|
576
|
+
config: new_config,
|
577
|
+
metadata: new_metadata,
|
578
|
+
dependencies: packwerk_package.dependencies.uniq.sort
|
579
|
+
)
|
580
|
+
|
581
|
+
ParsePackwerk.write_package_yml!(new_package)
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
sig { params(config: T::Hash[T.anything, T.anything]).returns(T::Hash[T.anything, T.anything]) }
|
586
|
+
def self.sort_keys(config)
|
587
|
+
sort_order = ParsePackwerk.key_sort_order
|
588
|
+
config.to_a.sort_by { |key, _value| T.unsafe(sort_order).index(key) }.to_h
|
589
|
+
end
|
590
|
+
|
591
|
+
sig { params(packs: T::Array[Packs::Pack]).void }
|
592
|
+
def self.visualize(packs: Packs.all)
|
593
|
+
VisualizePacks.package_graph!(packs)
|
594
|
+
end
|
595
|
+
|
596
|
+
sig { returns(String) }
|
597
|
+
def self.rename_pack
|
598
|
+
<<~WARNING
|
599
|
+
We do not yet have an automated API for this.
|
600
|
+
|
601
|
+
Follow these steps:
|
602
|
+
1. Rename the `packs/your_pack` directory to the name of the new pack, `packs/new_pack_name
|
603
|
+
2. Replace references to `- packs/your_pack` in `package.yml` files with `- packs/new_pack_name`
|
604
|
+
3. Rerun `bin/packwerk update-todo` to update violations
|
605
|
+
4. Run `bin/codeownership validate` to update ownership information
|
606
|
+
5. Please let us know if anything is missing.
|
607
|
+
WARNING
|
608
|
+
end
|
609
|
+
|
610
|
+
# This function exists to give us something to stub in test
|
611
|
+
sig { params(code: Integer).void }
|
612
|
+
def self.safe_exit(code)
|
613
|
+
exit code
|
12
614
|
end
|
13
615
|
end
|
14
616
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Packs
|
4
|
+
class RubocopPostProcessor
|
5
|
+
include PerFileProcessorInterface
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { override.params(file_move_operation: Private::FileMoveOperation).void }
|
9
|
+
def before_move_file!(file_move_operation)
|
10
|
+
return unless rubocop_enabled?
|
11
|
+
|
12
|
+
relative_path_to_origin = file_move_operation.origin_pathname
|
13
|
+
relative_path_to_destination = file_move_operation.destination_pathname
|
14
|
+
|
15
|
+
rubocop_todo = Pathname.new('.rubocop_todo.yml')
|
16
|
+
if rubocop_todo.exist?
|
17
|
+
Packs.replace_in_file(
|
18
|
+
file: rubocop_todo.to_s,
|
19
|
+
find: relative_path_to_origin,
|
20
|
+
replace_with: relative_path_to_destination
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(T::Boolean) }
|
26
|
+
def rubocop_enabled?
|
27
|
+
Pathname.new('.rubocop.yml').exist?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|