use_packs 0.0.1
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 +7 -0
- data/README.md +71 -0
- data/bin/packs +10 -0
- data/bin/rubocop +29 -0
- data/bin/tapioca +29 -0
- data/lib/use_packs/cli.rb +127 -0
- data/lib/use_packs/code_ownership_post_processor.rb +58 -0
- data/lib/use_packs/configuration.rb +61 -0
- data/lib/use_packs/default_user_event_logger.rb +7 -0
- data/lib/use_packs/logging.rb +37 -0
- data/lib/use_packs/per_file_processor_interface.rb +18 -0
- data/lib/use_packs/private/file_move_operation.rb +80 -0
- data/lib/use_packs/private/interactive_cli/pack_selector.rb +34 -0
- data/lib/use_packs/private/interactive_cli/team_selector.rb +35 -0
- data/lib/use_packs/private/interactive_cli/use_cases/add_dependency.rb +30 -0
- data/lib/use_packs/private/interactive_cli/use_cases/check.rb +25 -0
- data/lib/use_packs/private/interactive_cli/use_cases/create.rb +27 -0
- data/lib/use_packs/private/interactive_cli/use_cases/get_info.rb +74 -0
- data/lib/use_packs/private/interactive_cli/use_cases/interface.rb +34 -0
- data/lib/use_packs/private/interactive_cli/use_cases/lint_package_yml.rb +26 -0
- data/lib/use_packs/private/interactive_cli/use_cases/make_public.rb +34 -0
- data/lib/use_packs/private/interactive_cli/use_cases/move.rb +36 -0
- data/lib/use_packs/private/interactive_cli/use_cases/nest.rb +31 -0
- data/lib/use_packs/private/interactive_cli/use_cases/query.rb +51 -0
- data/lib/use_packs/private/interactive_cli/use_cases/regenerate_rubocop_todo.rb +26 -0
- data/lib/use_packs/private/interactive_cli/use_cases/rename.rb +34 -0
- data/lib/use_packs/private/interactive_cli/use_cases/update_deprecations.rb +25 -0
- data/lib/use_packs/private/interactive_cli/use_cases/validate.rb +25 -0
- data/lib/use_packs/private/interactive_cli/use_cases/visualize.rb +44 -0
- data/lib/use_packs/private/interactive_cli.rb +52 -0
- data/lib/use_packs/private/pack_relationship_analyzer.rb +135 -0
- data/lib/use_packs/private/packwerk_wrapper/offenses_aggregator_formatter.rb +34 -0
- data/lib/use_packs/private/packwerk_wrapper.rb +71 -0
- data/lib/use_packs/private.rb +453 -0
- data/lib/use_packs/rubocop_post_processor.rb +67 -0
- data/lib/use_packs/spring_command.rb +28 -0
- data/lib/use_packs/user_event_logger.rb +259 -0
- data/lib/use_packs.rb +298 -0
- metadata +351 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePacks
|
4
|
+
module Private
|
5
|
+
module PackwerkWrapper
|
6
|
+
#
|
7
|
+
# This formatter simply collects offenses so we can feed them into other systems
|
8
|
+
#
|
9
|
+
class OffensesAggregatorFormatter
|
10
|
+
extend T::Sig
|
11
|
+
include Packwerk::OffensesFormatter
|
12
|
+
|
13
|
+
sig { returns(T::Array[Packwerk::ReferenceOffense]) }
|
14
|
+
attr_reader :aggregated_offenses
|
15
|
+
|
16
|
+
sig { void }
|
17
|
+
def initialize
|
18
|
+
@aggregated_offenses = T.let([], T::Array[Packwerk::ReferenceOffense])
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { override.params(offenses: T::Array[T.nilable(Packwerk::Offense)]).returns(String) }
|
22
|
+
def show_offenses(offenses)
|
23
|
+
@aggregated_offenses = T.unsafe(offenses)
|
24
|
+
''
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { override.params(offense_collection: Packwerk::OffenseCollection, for_files: T::Set[String]).returns(String) }
|
28
|
+
def show_stale_violations(offense_collection, for_files)
|
29
|
+
''
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'packwerk'
|
4
|
+
require 'use_packs/private/packwerk_wrapper/offenses_aggregator_formatter'
|
5
|
+
|
6
|
+
module UsePacks
|
7
|
+
module Private
|
8
|
+
module PackwerkWrapper
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(argv: T.untyped, formatter: Packwerk::OffensesFormatter).void }
|
12
|
+
def self.packwerk_cli_run_safely(argv, formatter)
|
13
|
+
with_safe_exit_if_no_files_found do
|
14
|
+
packwerk_cli(formatter).run(argv)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# execute_command is like `run` except it does not `exit`
|
20
|
+
#
|
21
|
+
sig { params(argv: T.untyped, formatter: T.nilable(Packwerk::OffensesFormatter)).void }
|
22
|
+
def self.packwerk_cli_execute_safely(argv, formatter = nil)
|
23
|
+
with_safe_exit_if_no_files_found do
|
24
|
+
packwerk_cli(formatter).execute_command(argv)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(block: T.proc.returns(T.untyped)).void }
|
29
|
+
def self.with_safe_exit_if_no_files_found(&block)
|
30
|
+
block.call
|
31
|
+
rescue SystemExit => e
|
32
|
+
# Packwerk should probably exit positively here rather than raising an error -- there should be no
|
33
|
+
# errors if the user has excluded all files being checked.
|
34
|
+
unless e.message == 'No files found or given. Specify files or check the include and exclude glob in the config file.'
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { params(formatter: T.nilable(Packwerk::OffensesFormatter)).returns(Packwerk::Cli) }
|
40
|
+
def self.packwerk_cli(formatter)
|
41
|
+
# This is mostly copied from exe/packwerk within the packwerk gem, but we use our own formatters
|
42
|
+
# Note that packwerk does not allow you to pass in your own progress formatter currently
|
43
|
+
ENV['RAILS_ENV'] = 'test'
|
44
|
+
|
45
|
+
style = Packwerk::OutputStyles::Coloured.new
|
46
|
+
Packwerk::Cli.new(style: style, offenses_formatter: formatter)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(files: T::Array[String]).returns(T::Array[Packwerk::ReferenceOffense]) }
|
50
|
+
def self.get_offenses_for_files(files)
|
51
|
+
formatter = OffensesAggregatorFormatter.new
|
52
|
+
packwerk_cli_execute_safely(['check', *files], formatter)
|
53
|
+
formatter.aggregated_offenses.compact
|
54
|
+
end
|
55
|
+
|
56
|
+
sig { params(files: T::Array[String]).returns(T::Array[Packwerk::ReferenceOffense]) }
|
57
|
+
def self.get_offenses_for_files_by_package(files)
|
58
|
+
packages = package_names_for_files(files)
|
59
|
+
argv = ['check', '--packages', packages.join(',')]
|
60
|
+
formatter = OffensesAggregatorFormatter.new
|
61
|
+
packwerk_cli_execute_safely(argv, formatter)
|
62
|
+
formatter.aggregated_offenses.compact
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { params(files: T::Array[String]).returns(T::Array[String]) }
|
66
|
+
def self.package_names_for_files(files)
|
67
|
+
files.map { |f| ParsePackwerk.package_from_path(f).name }.compact.uniq
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,453 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'colorized_string'
|
6
|
+
require 'sorbet-runtime'
|
7
|
+
|
8
|
+
require 'use_packs/private/file_move_operation'
|
9
|
+
require 'use_packs/private/pack_relationship_analyzer'
|
10
|
+
require 'use_packs/private/interactive_cli'
|
11
|
+
require 'use_packs/private/packwerk_wrapper'
|
12
|
+
|
13
|
+
module UsePacks
|
14
|
+
module Private
|
15
|
+
extend T::Sig
|
16
|
+
|
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 = UsePacks.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 = UsePacks.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[UsePacks::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::DEPRECATED_REFERENCES_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
|
+
)
|
175
|
+
ParsePackwerk.write_package_yml!(new_package)
|
176
|
+
ParsePackwerk.bust_cache!
|
177
|
+
|
178
|
+
# Move everything from the old pack to the new one
|
179
|
+
move_to_pack!(
|
180
|
+
pack_name: new_package_name,
|
181
|
+
paths_relative_to_root: [package.directory.to_s],
|
182
|
+
per_file_processors: per_file_processors
|
183
|
+
)
|
184
|
+
|
185
|
+
# Then delete the old package.yml and deprecated_references.yml files
|
186
|
+
package.yml.delete
|
187
|
+
deprecated_references_file = ParsePackwerk::DeprecatedReferences.for(package).pathname
|
188
|
+
deprecated_references_file.delete if deprecated_references_file.exist?
|
189
|
+
|
190
|
+
ParsePackwerk.bust_cache!
|
191
|
+
|
192
|
+
ParsePackwerk.all.each do |other_package|
|
193
|
+
new_dependencies = other_package.dependencies.map { |d| d == pack_name ? new_package_name : d }
|
194
|
+
if other_package.name == parent_name && !new_dependencies.include?(new_package_name)
|
195
|
+
new_dependencies += [new_package_name]
|
196
|
+
end
|
197
|
+
|
198
|
+
new_other_package = ParsePackwerk::Package.new(
|
199
|
+
name: other_package.name,
|
200
|
+
enforce_privacy: other_package.enforce_privacy,
|
201
|
+
enforce_dependencies: other_package.enforce_dependencies,
|
202
|
+
dependencies: new_dependencies.uniq.sort,
|
203
|
+
metadata: other_package.metadata
|
204
|
+
)
|
205
|
+
|
206
|
+
ParsePackwerk.write_package_yml!(new_other_package)
|
207
|
+
end
|
208
|
+
|
209
|
+
sorbet_config = Pathname.new('sorbet/config')
|
210
|
+
if sorbet_config.exist?
|
211
|
+
UsePacks.replace_in_file(
|
212
|
+
file: sorbet_config.to_s,
|
213
|
+
find: package.directory.join('spec'),
|
214
|
+
replace_with: new_package.directory.join('spec')
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
sig do
|
220
|
+
params(
|
221
|
+
paths_relative_to_root: T::Array[String],
|
222
|
+
per_file_processors: T::Array[UsePacks::PerFileProcessorInterface]
|
223
|
+
).void
|
224
|
+
end
|
225
|
+
def self.make_public!(paths_relative_to_root:, per_file_processors:)
|
226
|
+
if paths_relative_to_root.any?
|
227
|
+
file_move_operations = T.let([], T::Array[Private::FileMoveOperation])
|
228
|
+
|
229
|
+
Logging.section('File Operations') do
|
230
|
+
file_paths = paths_relative_to_root.flat_map do |path|
|
231
|
+
origin_pathname = Pathname.new(path).cleanpath
|
232
|
+
if origin_pathname.directory?
|
233
|
+
origin_pathname.glob('**/*.*').map(&:to_s)
|
234
|
+
else
|
235
|
+
path
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
file_move_operations = file_paths.flat_map do |path|
|
240
|
+
package = ParsePackwerk.package_from_path(path)
|
241
|
+
origin_pathname = Pathname.new(path).cleanpath
|
242
|
+
|
243
|
+
file_move_operation = FileMoveOperation.new(
|
244
|
+
origin_pathname: origin_pathname,
|
245
|
+
destination_pathname: FileMoveOperation.destination_pathname_for_new_public_api(origin_pathname),
|
246
|
+
destination_pack: package
|
247
|
+
)
|
248
|
+
|
249
|
+
[
|
250
|
+
file_move_operation,
|
251
|
+
file_move_operation.spec_file_move_operation
|
252
|
+
]
|
253
|
+
end
|
254
|
+
|
255
|
+
file_move_operations.each do |file_move_operation|
|
256
|
+
Private.package_filepath(file_move_operation, per_file_processors)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
per_file_processors.each do |processor|
|
261
|
+
processor.after_move_files!(file_move_operations)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
sig do
|
267
|
+
params(
|
268
|
+
pack_name: String,
|
269
|
+
dependency_name: String
|
270
|
+
).void
|
271
|
+
end
|
272
|
+
def self.add_dependency!(pack_name:, dependency_name:)
|
273
|
+
all_packages = ParsePackwerk.all
|
274
|
+
|
275
|
+
pack_name = Private.clean_pack_name(pack_name)
|
276
|
+
package = all_packages.find { |p| p.name == pack_name }
|
277
|
+
if package.nil?
|
278
|
+
raise StandardError, "Can not find package with name #{pack_name}. Make sure the argument is of the form `packs/my_pack/`"
|
279
|
+
end
|
280
|
+
|
281
|
+
dependency_name = Private.clean_pack_name(dependency_name)
|
282
|
+
package_dependency = all_packages.find { |p| p.name == dependency_name }
|
283
|
+
if package_dependency.nil?
|
284
|
+
raise StandardError, "Can not find package with name #{dependency_name}. Make sure the argument is of the form `packs/my_pack/`"
|
285
|
+
end
|
286
|
+
|
287
|
+
new_package = ParsePackwerk::Package.new(
|
288
|
+
name: pack_name,
|
289
|
+
dependencies: (package.dependencies + [dependency_name]).uniq.sort,
|
290
|
+
enforce_privacy: package.enforce_privacy,
|
291
|
+
enforce_dependencies: package.enforce_dependencies,
|
292
|
+
metadata: package.metadata
|
293
|
+
)
|
294
|
+
ParsePackwerk.write_package_yml!(new_package)
|
295
|
+
end
|
296
|
+
|
297
|
+
sig { params(file_move_operation: FileMoveOperation, per_file_processors: T::Array[UsePacks::PerFileProcessorInterface]).void }
|
298
|
+
def self.package_filepath(file_move_operation, per_file_processors)
|
299
|
+
per_file_processors.each do |per_file_processor|
|
300
|
+
if file_move_operation.origin_pathname.exist?
|
301
|
+
per_file_processor.before_move_file!(file_move_operation)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
origin = file_move_operation.origin_pathname
|
306
|
+
destination = file_move_operation.destination_pathname
|
307
|
+
idempotent_mv(origin, destination)
|
308
|
+
end
|
309
|
+
|
310
|
+
sig { params(origin: Pathname, destination: Pathname).void }
|
311
|
+
def self.idempotent_mv(origin, destination)
|
312
|
+
if origin.exist? && destination.exist?
|
313
|
+
Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, #{destination} already exists").red
|
314
|
+
elsif origin.exist? && !destination.exist?
|
315
|
+
destination.dirname.mkpath
|
316
|
+
|
317
|
+
Logging.print "Moving file #{origin} to #{destination}"
|
318
|
+
# use git mv so that git knows that it was a move
|
319
|
+
FileUtils.mv(origin, destination)
|
320
|
+
elsif !origin.exist? && destination.exist?
|
321
|
+
Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, does not exist, (#{destination} already exists)").red
|
322
|
+
else
|
323
|
+
Logging.print ColorizedString.new("[SKIP] Not moving #{origin}, does not exist").red
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
sig { params(package: ParsePackwerk::Package).void }
|
328
|
+
def self.add_public_directory(package)
|
329
|
+
public_directory = package.directory.join('app/public')
|
330
|
+
|
331
|
+
if public_directory.glob('**/**.rb').none?
|
332
|
+
FileUtils.mkdir_p(public_directory)
|
333
|
+
todo_md = UsePacks.config.user_event_logger.on_create_public_directory_todo(package.name)
|
334
|
+
public_directory.join('TODO.md').write(todo_md)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
sig { params(package: ParsePackwerk::Package).void }
|
339
|
+
def self.add_readme_todo(package)
|
340
|
+
pack_directory = package.directory
|
341
|
+
|
342
|
+
if !pack_directory.join('README.md').exist?
|
343
|
+
readme_todo_md = UsePacks.config.user_event_logger.on_create_readme_todo(package.name)
|
344
|
+
pack_directory.join('README_TODO.md').write(readme_todo_md)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
sig do
|
349
|
+
params(
|
350
|
+
pack_name: String,
|
351
|
+
enforce_privacy: T::Boolean,
|
352
|
+
enforce_dependencies: T.nilable(T::Boolean),
|
353
|
+
team: T.nilable(CodeTeams::Team)
|
354
|
+
).returns(ParsePackwerk::Package)
|
355
|
+
end
|
356
|
+
def self.create_pack_if_not_exists!(pack_name:, enforce_privacy:, enforce_dependencies:, team: nil)
|
357
|
+
if PERMITTED_PACK_LOCATIONS.none? { |permitted_location| pack_name.start_with?(permitted_location) }
|
358
|
+
raise StandardError, "UsePacks 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`."
|
359
|
+
end
|
360
|
+
|
361
|
+
existing_package = ParsePackwerk.all.find { |p| p.name == pack_name }
|
362
|
+
if existing_package.nil?
|
363
|
+
should_enforce_dependenceies = enforce_dependencies.nil? ? UsePacks.config.enforce_dependencies : enforce_dependencies
|
364
|
+
|
365
|
+
package = ParsePackwerk::Package.new(
|
366
|
+
enforce_dependencies: should_enforce_dependenceies,
|
367
|
+
enforce_privacy: enforce_privacy,
|
368
|
+
dependencies: [],
|
369
|
+
metadata: {
|
370
|
+
'owner' => team.nil? ? 'MyTeam' : team.name
|
371
|
+
},
|
372
|
+
name: pack_name
|
373
|
+
)
|
374
|
+
|
375
|
+
ParsePackwerk.write_package_yml!(package)
|
376
|
+
RuboCop::Packs.set_default_rubocop_yml(packs: [package])
|
377
|
+
|
378
|
+
current_contents = package.yml.read
|
379
|
+
new_contents = current_contents.gsub('MyTeam', 'MyTeam # specify your team here, or delete this key if this package is not owned by one team')
|
380
|
+
package.yml.write(new_contents)
|
381
|
+
existing_package = package
|
382
|
+
end
|
383
|
+
|
384
|
+
existing_package
|
385
|
+
end
|
386
|
+
|
387
|
+
sig { void }
|
388
|
+
def self.load_client_configuration
|
389
|
+
@loaded_client_configuration ||= T.let(false, T.nilable(T::Boolean))
|
390
|
+
return if @loaded_client_configuration
|
391
|
+
|
392
|
+
@loaded_client_configuration = true
|
393
|
+
client_configuration = Pathname.pwd.join('config/use_packs.rb')
|
394
|
+
require client_configuration.to_s if client_configuration.exist?
|
395
|
+
end
|
396
|
+
|
397
|
+
sig { void }
|
398
|
+
def self.bust_cache!
|
399
|
+
UsePacks.config.bust_cache!
|
400
|
+
# This comes explicitly after `UsePacks.config.bust_cache!` because
|
401
|
+
# otherwise `UsePacks.config` will attempt to reload the client configuratoin.
|
402
|
+
@loaded_client_configuration = false
|
403
|
+
end
|
404
|
+
|
405
|
+
sig { returns(T::Hash[String, String]) }
|
406
|
+
def self.get_deprecated_references_contents
|
407
|
+
deprecated_references = {}
|
408
|
+
ParsePackwerk.all.each do |package|
|
409
|
+
deprecated_references_yml = ParsePackwerk::DeprecatedReferences.for(package).pathname
|
410
|
+
if deprecated_references_yml.exist?
|
411
|
+
deprecated_references[deprecated_references_yml.to_s] = deprecated_references_yml.read
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
deprecated_references
|
416
|
+
end
|
417
|
+
|
418
|
+
DeprecatedReferencesFiles = T.type_alias do
|
419
|
+
T::Hash[String, T.nilable(String)]
|
420
|
+
end
|
421
|
+
|
422
|
+
sig { params(before: DeprecatedReferencesFiles, after: DeprecatedReferencesFiles).returns(String) }
|
423
|
+
def self.diff_deprecated_references_yml(before, after)
|
424
|
+
dir_containing_contents_before = Dir.mktmpdir
|
425
|
+
dir_containing_contents_after = Dir.mktmpdir
|
426
|
+
begin
|
427
|
+
write_deprecated_references_to_tmp_folder(before, dir_containing_contents_before)
|
428
|
+
write_deprecated_references_to_tmp_folder(after, dir_containing_contents_after)
|
429
|
+
|
430
|
+
diff = `diff -r #{dir_containing_contents_before}/ #{dir_containing_contents_after}/`
|
431
|
+
# For ease of reading, sub out the tmp directory from the diff
|
432
|
+
diff.gsub(dir_containing_contents_before, '').gsub(dir_containing_contents_after, '')
|
433
|
+
ensure
|
434
|
+
FileUtils.remove_entry dir_containing_contents_before
|
435
|
+
FileUtils.remove_entry dir_containing_contents_after
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
sig { params(deprecated_references_files: DeprecatedReferencesFiles, tmp_folder: String).void }
|
440
|
+
def self.write_deprecated_references_to_tmp_folder(deprecated_references_files, tmp_folder)
|
441
|
+
deprecated_references_files.each do |filename, contents|
|
442
|
+
next if contents.nil?
|
443
|
+
|
444
|
+
tmp_folder_pathname = Pathname.new(tmp_folder)
|
445
|
+
temp_deprecated_references_yml = tmp_folder_pathname.join(filename)
|
446
|
+
FileUtils.mkdir_p(temp_deprecated_references_yml.dirname)
|
447
|
+
temp_deprecated_references_yml.write(contents)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
private_constant :Private
|
453
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module UsePacks
|
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
|
+
relative_path_to_origin = file_move_operation.origin_pathname
|
11
|
+
relative_path_to_destination = file_move_operation.destination_pathname
|
12
|
+
|
13
|
+
rubocop_todo = Pathname.new('.rubocop_todo.yml')
|
14
|
+
if rubocop_todo.exist?
|
15
|
+
UsePacks.replace_in_file(
|
16
|
+
file: rubocop_todo.to_s,
|
17
|
+
find: relative_path_to_origin,
|
18
|
+
replace_with: relative_path_to_destination
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
if file_move_operation.origin_pack.name != ParsePackwerk::ROOT_PACKAGE_NAME && file_move_operation.destination_pack.name != ParsePackwerk::ROOT_PACKAGE_NAME
|
23
|
+
origin_rubocop_todo = file_move_operation.origin_pack.directory.join(RuboCop::Packs::PACK_LEVEL_RUBOCOP_TODO_YML)
|
24
|
+
# If there were TODOs for this file in the origin pack's pack-based rubocop, we want to move it to the destination
|
25
|
+
if origin_rubocop_todo.exist?
|
26
|
+
loaded_origin_rubocop_todo = YAML.load_file(origin_rubocop_todo)
|
27
|
+
new_origin_rubocop_todo = loaded_origin_rubocop_todo.dup
|
28
|
+
|
29
|
+
loaded_origin_rubocop_todo.each do |cop_name, cop_config|
|
30
|
+
next unless cop_config['Exclude'].include?(relative_path_to_origin.to_s)
|
31
|
+
|
32
|
+
new_origin_rubocop_todo[cop_name]['Exclude'] = cop_config['Exclude'] - [relative_path_to_origin.to_s]
|
33
|
+
origin_rubocop_todo.write(YAML.dump(new_origin_rubocop_todo))
|
34
|
+
|
35
|
+
destination_rubocop_todo = file_move_operation.destination_pack.directory.join(RuboCop::Packs::PACK_LEVEL_RUBOCOP_TODO_YML)
|
36
|
+
if destination_rubocop_todo.exist?
|
37
|
+
new_destination_rubocop_todo = YAML.load_file(destination_rubocop_todo).dup
|
38
|
+
else
|
39
|
+
new_destination_rubocop_todo = {}
|
40
|
+
end
|
41
|
+
|
42
|
+
new_destination_rubocop_todo[cop_name] ||= { 'Exclude' => [] }
|
43
|
+
|
44
|
+
new_destination_rubocop_todo[cop_name]['Exclude'] += [relative_path_to_destination.to_s]
|
45
|
+
destination_rubocop_todo.write(YAML.dump(new_destination_rubocop_todo))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { params(file_move_operations: T::Array[Private::FileMoveOperation]).void }
|
52
|
+
def after_move_files!(file_move_operations)
|
53
|
+
# There could also be no TODOs for this file, but moving it produced TODOs. This could happen if:
|
54
|
+
# 1) The origin pack did not enforce a rubocop, such as typed public APIs
|
55
|
+
# 2) The file satisfied the cop in the origin pack, such as the Packs/RootNamespaceIsPackName, but the desired
|
56
|
+
# namespace changed once the file was moved to a different pack.
|
57
|
+
files = []
|
58
|
+
file_move_operations.each do |file_move_operation|
|
59
|
+
if file_move_operation.destination_pathname.exist?
|
60
|
+
files << file_move_operation.destination_pathname.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
RuboCop::Packs.regenerate_todo(files: files)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'spring/commands'
|
5
|
+
|
6
|
+
module UsePacks
|
7
|
+
class SpringCommand
|
8
|
+
def env(*)
|
9
|
+
# Packwerk needs to run in a test environment, which has a set of autoload paths that are
|
10
|
+
# often a superset of the dev/prod paths (for example, test/support/helpers)
|
11
|
+
'test'
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec_name
|
15
|
+
'packs'
|
16
|
+
end
|
17
|
+
|
18
|
+
def gem_name
|
19
|
+
'use_packs'
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
load(Gem.bin_path(gem_name, exec_name))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Spring.register_command('packs', SpringCommand.new)
|
28
|
+
end
|