freyia 0.5.0

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.
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Freyia
6
+ module Automations
7
+ # Copies the file from the relative source to the relative destination. If
8
+ # the destination is not given it's assumed to be equal to the source.
9
+ #
10
+ # ==== Parameters
11
+ # source<String>:: the relative path to the source root.
12
+ # destination<String>:: the relative path to the destination root.
13
+ # config<Hash>:: give :verbose => false to not log the status, and
14
+ # :mode => :preserve, to preserve the file mode from the source.
15
+ #
16
+ # ==== Examples
17
+ #
18
+ # copy_file "README", "doc/README"
19
+ #
20
+ # copy_file "doc/README"
21
+ #
22
+ def copy_file(source, *args, &block)
23
+ config = args.last.is_a?(Hash) ? args.pop : {}
24
+ destination = args.first || source
25
+ source = File.expand_path(find_in_source_paths(source.to_s))
26
+
27
+ resulting_destination = create_file destination, nil, config do
28
+ content = File.binread(source)
29
+ content = yield(content) if block
30
+ content
31
+ end
32
+ return unless config[:mode] == :preserve
33
+
34
+ mode = File.stat(source).mode
35
+ chmod(resulting_destination, mode, config)
36
+ end
37
+
38
+ # Links the file from the relative source to the relative destination. If
39
+ # the destination is not given it's assumed to be equal to the source.
40
+ #
41
+ # ==== Parameters
42
+ # source<String>:: the relative path to the source root.
43
+ # destination<String>:: the relative path to the destination root.
44
+ # config<Hash>:: give :verbose => false to not log the status.
45
+ #
46
+ # ==== Examples
47
+ #
48
+ # link_file "README", "doc/README"
49
+ #
50
+ # link_file "doc/README"
51
+ #
52
+ def link_file(source, *args)
53
+ config = args.last.is_a?(Hash) ? args.pop : {}
54
+ destination = args.first || source
55
+ source = File.expand_path(find_in_source_paths(source.to_s))
56
+
57
+ create_link destination, source, config
58
+ end
59
+
60
+ # Gets the content at the given address and places it at the given relative
61
+ # destination. If a block is given instead of destination, the content of
62
+ # the url is yielded and used as location.
63
+ #
64
+ # +get+ relies on open-uri, so passing application user input would provide
65
+ # a command injection attack vector.
66
+ #
67
+ # ==== Parameters
68
+ # source<String>:: the address of the given content.
69
+ # destination<String>:: the relative path to the destination root.
70
+ # config<Hash>:: give :verbose => false to not log the status, and
71
+ # :http_headers => <Hash> to add headers to an http request.
72
+ #
73
+ # ==== Examples
74
+ #
75
+ # get "http://gist.github.com/103208", "doc/README"
76
+ #
77
+ # get "http://gist.github.com/103208", "doc/README", :http_headers => {"Content-Type" => "application/json"}
78
+ #
79
+ # get "http://gist.github.com/103208" do |content|
80
+ # content.split("\n").first
81
+ # end
82
+ #
83
+ def get(source, *args, &block) # rubocop:todo Metrics
84
+ config = args.last.is_a?(Hash) ? args.pop : {}
85
+ destination = args.first
86
+
87
+ render = if %r{^https?://}.match?(source)
88
+ require "open-uri"
89
+ URI.send(:open, source, config.fetch(:http_headers, {})) do |input|
90
+ input.binmode.read
91
+ end
92
+ else
93
+ source = File.expand_path(find_in_source_paths(source.to_s))
94
+ File.open(source) { |input| input.binmode.read }
95
+ end
96
+
97
+ destination ||= if block_given?
98
+ block.arity == 1 ? yield(render) : yield
99
+ else
100
+ File.basename(source)
101
+ end
102
+
103
+ create_file destination, render, config
104
+ end
105
+
106
+ # Gets an ERB template at the relative source, executes it and makes a copy
107
+ # at the relative destination. If the destination is not given it's assumed
108
+ # to be equal to the source removing .tt from the filename.
109
+ #
110
+ # ==== Parameters
111
+ # source<String>:: the relative path to the source root.
112
+ # destination<String>:: the relative path to the destination root.
113
+ # config<Hash>:: give :verbose => false to not log the status.
114
+ #
115
+ # ==== Examples
116
+ #
117
+ # template "README", "doc/README"
118
+ #
119
+ # template "doc/README"
120
+ #
121
+ def template(source, *args, &block)
122
+ config = args.last.is_a?(Hash) ? args.pop : {}
123
+ destination = args.first || source.sub(%r{#{TEMPLATE_EXTNAME}$}o, "")
124
+
125
+ source = File.expand_path(find_in_source_paths(source.to_s))
126
+ context = config.delete(:context) || instance_eval("binding", __FILE__, __LINE__)
127
+
128
+ create_file destination, nil, config do
129
+ capturable_erb = CapturableERB.new(::File.binread(source), trim_mode: "-",
130
+ eoutvar: "@output_buffer")
131
+ content = capturable_erb.tap do |erb|
132
+ erb.filename = source
133
+ end.result(context)
134
+ content = yield(content) if block
135
+ content
136
+ end
137
+ end
138
+
139
+ # Changes the mode of the given file or directory.
140
+ #
141
+ # ==== Parameters
142
+ # mode<Integer>:: the file mode
143
+ # path<String>:: the name of the file to change mode
144
+ # config<Hash>:: give :verbose => false to not log the status.
145
+ #
146
+ # ==== Example
147
+ #
148
+ # chmod "script/server", 0755
149
+ #
150
+ def chmod(path, mode, config = {})
151
+ path = File.expand_path(path, destination_root)
152
+ say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true)
153
+ return if options[:pretend]
154
+
155
+ require "fileutils"
156
+ FileUtils.chmod_R(mode, path)
157
+ end
158
+
159
+ # Prepend text to a file. Since it depends on insert_into_file, it's reversible.
160
+ #
161
+ # ==== Parameters
162
+ # path<String>:: path of the file to be changed
163
+ # data<String>:: the data to prepend to the file, can be also given as a block.
164
+ # config<Hash>:: give :verbose => false to not log the status.
165
+ #
166
+ # ==== Example
167
+ #
168
+ # prepend_to_file 'config/environments/test.rb', 'config.gem "rspec"'
169
+ #
170
+ # prepend_to_file 'config/environments/test.rb' do
171
+ # 'config.gem "rspec"'
172
+ # end
173
+ #
174
+ def prepend_to_file(path, *args, &)
175
+ config = args.last.is_a?(Hash) ? args.pop : {}
176
+ config[:after] = %r{\A}
177
+ insert_into_file(path, *(args << config), &)
178
+ end
179
+ alias_method :prepend_file, :prepend_to_file
180
+
181
+ # Append text to a file. Since it depends on insert_into_file, it's reversible.
182
+ #
183
+ # ==== Parameters
184
+ # path<String>:: path of the file to be changed
185
+ # data<String>:: the data to append to the file, can be also given as a block.
186
+ # config<Hash>:: give :verbose => false to not log the status.
187
+ #
188
+ # ==== Example
189
+ #
190
+ # append_to_file 'config/environments/test.rb', 'config.gem "rspec"'
191
+ #
192
+ # append_to_file 'config/environments/test.rb' do
193
+ # 'config.gem "rspec"'
194
+ # end
195
+ #
196
+ def append_to_file(path, *args, &)
197
+ config = args.last.is_a?(Hash) ? args.pop : {}
198
+ config[:before] = %r{\z}
199
+ insert_into_file(path, *(args << config), &)
200
+ end
201
+ alias_method :append_file, :append_to_file
202
+
203
+ # Injects text right after the class definition. Since it depends on
204
+ # insert_into_file, it's reversible.
205
+ #
206
+ # ==== Parameters
207
+ # path<String>:: path of the file to be changed
208
+ # klass<String|Class>:: the class to be manipulated
209
+ # data<String>:: the data to append to the class, can be also given as a block.
210
+ # config<Hash>:: give :verbose => false to not log the status.
211
+ #
212
+ # ==== Examples
213
+ #
214
+ # inject_into_class "app/controllers/application_controller.rb",
215
+ # "ApplicationController",
216
+ # " filter_parameter :password\n"
217
+ #
218
+ # inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
219
+ # " filter_parameter :password\n"
220
+ # end
221
+ #
222
+ def inject_into_class(path, klass, *args, &)
223
+ config = args.last.is_a?(Hash) ? args.pop : {}
224
+ config[:after] = %r{class #{klass}\n|class #{klass} .*\n}
225
+ insert_into_file(path, *(args << config), &)
226
+ end
227
+
228
+ # Injects text right after the module definition. Since it depends on
229
+ # insert_into_file, it's reversible.
230
+ #
231
+ # ==== Parameters
232
+ # path<String>:: path of the file to be changed
233
+ # module_name<String|Class>:: the module to be manipulated
234
+ # data<String>:: the data to append to the class, can be also given as a block.
235
+ # config<Hash>:: give :verbose => false to not log the status.
236
+ #
237
+ # ==== Examples
238
+ #
239
+ # inject_into_module "app/helpers/application_helper.rb",
240
+ # "ApplicationHelper",
241
+ # " def help; 'help'; end\n"
242
+ #
243
+ # inject_into_module "app/helpers/application_helper.rb", "ApplicationHelper" do
244
+ # " def help; 'help'; end\n"
245
+ # end
246
+ #
247
+ def inject_into_module(path, module_name, *args, &)
248
+ config = args.last.is_a?(Hash) ? args.pop : {}
249
+ config[:after] = %r{module #{module_name}\n|module #{module_name} .*\n}
250
+ insert_into_file(path, *(args << config), &)
251
+ end
252
+
253
+ # Run a regular expression replacement on a file, raising an error if the
254
+ # contents of the file are not changed.
255
+ #
256
+ # ==== Parameters
257
+ # path<String>:: path of the file to be changed
258
+ # flag<Regexp|String>:: the regexp or string to be replaced
259
+ # replacement<String>:: the replacement, can be also given as a block
260
+ # config<Hash>:: give :verbose => false to not log the status, and
261
+ # :force => true, to force the replacement regardless of runner behavior.
262
+ #
263
+ # ==== Example
264
+ #
265
+ # gsub_file! 'app/controllers/application_controller.rb',
266
+ # /#\s*(filter_parameter_logging :password)/, '\1'
267
+ #
268
+ # gsub_file! 'README', /rake/, :green do |match|
269
+ # match << " no more. Use freyia!"
270
+ # end
271
+ #
272
+ def gsub_file!(path, flag, *args, &)
273
+ config = args.last.is_a?(Hash) ? args.pop : {}
274
+
275
+ path = File.expand_path(path, destination_root)
276
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
277
+
278
+ actually_gsub_file(path, flag, args, true, &) unless options[:pretend]
279
+ end
280
+
281
+ # Run a regular expression replacement on a file.
282
+ #
283
+ # ==== Parameters
284
+ # path<String>:: path of the file to be changed
285
+ # flag<Regexp|String>:: the regexp or string to be replaced
286
+ # replacement<String>:: the replacement, can be also given as a block
287
+ # config<Hash>:: give :verbose => false to not log the status, and
288
+ # :force => true, to force the replacement regardless of runner behavior.
289
+ #
290
+ # ==== Example
291
+ #
292
+ # gsub_file 'app/controllers/application_controller.rb',
293
+ # /#\s*(filter_parameter_logging :password)/, '\1'
294
+ #
295
+ # gsub_file 'README', /rake/, :green do |match|
296
+ # match << " no more. Use freyia!"
297
+ # end
298
+ #
299
+ def gsub_file(path, flag, *args, &)
300
+ config = args.last.is_a?(Hash) ? args.pop : {}
301
+
302
+ path = File.expand_path(path, destination_root)
303
+ say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true)
304
+
305
+ actually_gsub_file(path, flag, args, false, &) unless options[:pretend]
306
+ end
307
+
308
+ # Uncomment all lines matching a given regex. Preserves indentation before
309
+ # the comment hash and removes the hash and any immediate following space.
310
+ #
311
+ # ==== Parameters
312
+ # path<String>:: path of the file to be changed
313
+ # flag<Regexp|String>:: the regexp or string used to decide which lines to uncomment
314
+ # config<Hash>:: give :verbose => false to not log the status.
315
+ #
316
+ # ==== Example
317
+ #
318
+ # uncomment_lines 'config/initializers/session_store.rb', /active_record/
319
+ #
320
+ def uncomment_lines(path, flag, *)
321
+ flag = flag.source if flag.respond_to?(:source)
322
+
323
+ gsub_file(path, %r{^(\s*)#[[:blank:]]?(.*#{flag})}, '\1\2', *)
324
+ end
325
+
326
+ # Comment all lines matching a given regex. It will leave the space
327
+ # which existed before the beginning of the line in tact and will insert
328
+ # a single space after the comment hash.
329
+ #
330
+ # ==== Parameters
331
+ # path<String>:: path of the file to be changed
332
+ # flag<Regexp|String>:: the regexp or string used to decide which lines to comment
333
+ # config<Hash>:: give :verbose => false to not log the status.
334
+ #
335
+ # ==== Example
336
+ #
337
+ # comment_lines 'config/initializers/session_store.rb', /cookie_store/
338
+ #
339
+ def comment_lines(path, flag, *)
340
+ flag = flag.source if flag.respond_to?(:source)
341
+
342
+ gsub_file(path, %r{^(\s*)([^#\n]*#{flag})}, '\1# \2', *)
343
+ end
344
+
345
+ # Removes a file at the given location.
346
+ #
347
+ # ==== Parameters
348
+ # path<String>:: path of the file to be changed
349
+ # config<Hash>:: give :verbose => false to not log the status.
350
+ #
351
+ # ==== Example
352
+ #
353
+ # remove_file 'README'
354
+ # remove_file 'app/controllers/application_controller.rb'
355
+ #
356
+ def remove_file(path, config = {})
357
+ return unless behavior == :invoke
358
+
359
+ path = File.expand_path(path, destination_root)
360
+
361
+ say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true)
362
+ return unless !options[:pretend] && (File.exist?(path) || File.symlink?(path))
363
+
364
+ require "fileutils"
365
+ ::FileUtils.rm_rf(path)
366
+ end
367
+ alias_method :remove_dir, :remove_file
368
+
369
+ attr_accessor :output_buffer
370
+ private :output_buffer, :output_buffer=
371
+
372
+ private
373
+
374
+ def concat(string)
375
+ @output_buffer.concat(string)
376
+ end
377
+
378
+ def capture(*args)
379
+ with_output_buffer { yield(*args) }
380
+ end
381
+
382
+ def with_output_buffer(buf = +"") #:nodoc:
383
+ raise ArgumentError, "Buffer cannot be a frozen object" if buf.frozen?
384
+
385
+ old_buffer = output_buffer
386
+ self.output_buffer = buf
387
+ yield
388
+ output_buffer
389
+ ensure
390
+ self.output_buffer = old_buffer
391
+ end
392
+
393
+ def actually_gsub_file(path, flag, args, error_on_no_change, &)
394
+ content = File.binread(path)
395
+ success = content.gsub!(flag, *args, &)
396
+
397
+ if success.nil? && error_on_no_change
398
+ raise Freyia::Error, "The content of #{path} did not change"
399
+ end
400
+
401
+ File.binwrite(path, content)
402
+ end
403
+
404
+ # Freyia::Automations#capture depends on what kind of buffer is used in ERB.
405
+ # Thus CapturableERB fixes ERB to use String buffer.
406
+ class CapturableERB < ERB
407
+ def set_eoutvar(compiler, eoutvar = "_erbout")
408
+ compiler.put_cmd = "#{eoutvar}.concat"
409
+ compiler.insert_cmd = "#{eoutvar}.concat"
410
+ compiler.pre_cmd = ["#{eoutvar} = ''.dup"]
411
+ compiler.post_cmd = [eoutvar]
412
+ end
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "empty_directory"
4
+
5
+ module Freyia
6
+ module Automations
7
+ WARNINGS = {
8
+ unchanged_no_flag: "File unchanged! Either the supplied flag value not found or the " \
9
+ "content has already been inserted!",
10
+ }.freeze
11
+
12
+ # Injects the given content into a file, raising an error if the contents of
13
+ # the file are not changed. Different from gsub_file, this method is reversible.
14
+ #
15
+ # ==== Parameters
16
+ # destination<String>:: Relative path to the destination root
17
+ # data<String>:: Data to add to the file. Can be given as a block.
18
+ # config<Hash>:: give :verbose => false to not log the status and the flag
19
+ # for injection (:after or :before) or :force => true for
20
+ # insert two or more times the same content.
21
+ #
22
+ # ==== Examples
23
+ #
24
+ # insert_into_file "config/environment.rb", "config.gem :freyia",
25
+ # after: "Rails::Initializer.run do |config|\n"
26
+ #
27
+ # insert_into_file "config/environment.rb", after: "Rails::Initializer.run do |config|\n" do
28
+ # gems = ask "Which gems would you like to add?"
29
+ # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
30
+ # end
31
+ #
32
+ def insert_into_file!(destination, *args, &block)
33
+ data = block_given? ? block : args.shift
34
+
35
+ config = args.shift || {}
36
+ config[:after] = %r{\z} unless config.key?(:before) || config.key?(:after)
37
+ config = config.merge({ error_on_no_change: true })
38
+
39
+ action InjectIntoFile.new(self, destination, data, config)
40
+ end
41
+ alias_method :inject_into_file!, :insert_into_file!
42
+
43
+ # Injects the given content into a file. Different from gsub_file, this
44
+ # method is reversible.
45
+ #
46
+ # ==== Parameters
47
+ # destination<String>:: Relative path to the destination root
48
+ # data<String>:: Data to add to the file. Can be given as a block.
49
+ # config<Hash>:: give :verbose => false to not log the status and the flag
50
+ # for injection (:after or :before) or :force => true for
51
+ # insert two or more times the same content.
52
+ #
53
+ # ==== Examples
54
+ #
55
+ # insert_into_file "config/environment.rb", "config.gem :freyia",
56
+ # after: "Rails::Initializer.run do |config|\n"
57
+ #
58
+ # insert_into_file "config/environment.rb", after: "Rails::Initializer.run do |config|\n" do
59
+ # gems = ask "Which gems would you like to add?"
60
+ # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n")
61
+ # end
62
+ #
63
+ def insert_into_file(destination, *args, &block)
64
+ data = block_given? ? block : args.shift
65
+
66
+ config = args.shift || {}
67
+ config[:after] = %r{\z} unless config.key?(:before) || config.key?(:after)
68
+
69
+ action InjectIntoFile.new(self, destination, data, config)
70
+ end
71
+ alias_method :inject_into_file, :insert_into_file
72
+
73
+ class InjectIntoFile < EmptyDirectory #:nodoc:
74
+ attr_reader :replacement, :flag, :behavior
75
+
76
+ def initialize(base, destination, data, config)
77
+ super(base, destination, { verbose: true }.merge(config))
78
+
79
+ @behavior, @flag = if @config.key?(:after)
80
+ [:after, @config.delete(:after)]
81
+ else
82
+ [:before, @config.delete(:before)]
83
+ end
84
+
85
+ @replacement = data.is_a?(Proc) ? data.call : data
86
+ @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp)
87
+ @error_on_no_change = @config.fetch(:error_on_no_change, false)
88
+ end
89
+
90
+ def invoke! # rubocop:todo Metrics
91
+ content = if @behavior == :after
92
+ "\\0#{replacement}"
93
+ else
94
+ "#{replacement}\\0"
95
+ end
96
+
97
+ if exists?
98
+ if replace!(%r{#{flag}}, content, config[:force])
99
+ say_status(:invoke)
100
+ elsif @error_on_no_change
101
+ raise Freyia::Error, "The content of #{destination} did not change"
102
+ elsif replacement_present?
103
+ say_status(:unchanged, color: :blue)
104
+ else
105
+ say_status(:unchanged, warning: WARNINGS[:unchanged_no_flag], color: :red)
106
+ end
107
+ else
108
+ raise Freyia::Error, "The file #{destination} does not appear to exist" unless pretend?
109
+ end
110
+ end
111
+
112
+ def revoke!
113
+ say_status :revoke
114
+
115
+ regexp = if @behavior == :after
116
+ content = '\1\2'
117
+ %r{(#{flag})(.*)(#{Regexp.escape(replacement)})}m
118
+ else
119
+ content = '\2\3'
120
+ %r{(#{Regexp.escape(replacement)})(.*)(#{flag})}m
121
+ end
122
+
123
+ replace!(regexp, content, true)
124
+ end
125
+
126
+ protected
127
+
128
+ def say_status(behavior, warning: nil, color: nil) # rubocop:todo Metrics
129
+ status = if behavior == :invoke
130
+ if flag == %r{\A}
131
+ :prepend
132
+ elsif flag == %r{\z}
133
+ :append
134
+ else
135
+ :insert
136
+ end
137
+ elsif warning
138
+ warning
139
+ elsif behavior == :unchanged
140
+ :unchanged
141
+ else
142
+ :subtract
143
+ end
144
+
145
+ super(status, color || config[:verbose])
146
+ end
147
+
148
+ def content
149
+ @content ||= File.read(destination)
150
+ end
151
+
152
+ def replacement_present?
153
+ content.include?(replacement)
154
+ end
155
+
156
+ # Adds the content to the file.
157
+ #
158
+ def replace!(regexp, string, force)
159
+ return unless force || !replacement_present?
160
+
161
+ success = content.gsub!(regexp, string)
162
+
163
+ File.binwrite(destination, content) unless pretend?
164
+ success
165
+ end
166
+ end
167
+ end
168
+ end