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.
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