packs 0.0.5 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -12
  3. data/bin/packs +10 -0
  4. data/bin/rubocop +29 -0
  5. data/bin/tapioca +29 -0
  6. data/lib/packs/cli.rb +164 -0
  7. data/lib/packs/code_ownership_post_processor.rb +58 -0
  8. data/lib/packs/configuration.rb +61 -0
  9. data/lib/packs/default_user_event_logger.rb +7 -0
  10. data/lib/packs/logging.rb +37 -0
  11. data/lib/packs/per_file_processor_interface.rb +18 -0
  12. data/lib/packs/private/file_move_operation.rb +80 -0
  13. data/lib/packs/private/interactive_cli/file_selector.rb +26 -0
  14. data/lib/packs/private/interactive_cli/pack_selector.rb +55 -0
  15. data/lib/packs/private/interactive_cli/team_selector.rb +58 -0
  16. data/lib/packs/private/interactive_cli/use_cases/add_dependency.rb +30 -0
  17. data/lib/packs/private/interactive_cli/use_cases/check.rb +25 -0
  18. data/lib/packs/private/interactive_cli/use_cases/create.rb +27 -0
  19. data/lib/packs/private/interactive_cli/use_cases/get_info.rb +37 -0
  20. data/lib/packs/private/interactive_cli/use_cases/interface.rb +34 -0
  21. data/lib/packs/private/interactive_cli/use_cases/lint_package_todo_yml_files.rb +25 -0
  22. data/lib/packs/private/interactive_cli/use_cases/lint_package_yml_files.rb +26 -0
  23. data/lib/packs/private/interactive_cli/use_cases/make_public.rb +30 -0
  24. data/lib/packs/private/interactive_cli/use_cases/move.rb +32 -0
  25. data/lib/packs/private/interactive_cli/use_cases/move_to_parent.rb +31 -0
  26. data/lib/packs/private/interactive_cli/use_cases/query.rb +51 -0
  27. data/lib/packs/private/interactive_cli/use_cases/rename.rb +25 -0
  28. data/lib/packs/private/interactive_cli/use_cases/update.rb +25 -0
  29. data/lib/packs/private/interactive_cli/use_cases/validate.rb +25 -0
  30. data/lib/packs/private/interactive_cli/use_cases/visualize.rb +44 -0
  31. data/lib/packs/private/interactive_cli.rb +52 -0
  32. data/lib/packs/private/pack_relationship_analyzer.rb +135 -0
  33. data/lib/packs/private/packwerk_wrapper/offenses_aggregator_formatter.rb +44 -0
  34. data/lib/packs/private/packwerk_wrapper.rb +70 -0
  35. data/lib/packs/private.rb +606 -4
  36. data/lib/packs/rubocop_post_processor.rb +30 -0
  37. data/lib/packs/user_event_logger.rb +199 -0
  38. data/lib/packs.rb +233 -53
  39. metadata +225 -14
  40. data/lib/packs/pack.rb +0 -43
  41. data/lib/packs/private/configuration.rb +0 -36
  42. data/lib/packs/rspec/fixture_helper.rb +0 -33
  43. 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 'packs/private/configuration'
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(Pathname) }
10
- def self.root
11
- Pathname.pwd
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